From 10bb3e7a74e187e49c4dd86147534903915c285c Mon Sep 17 00:00:00 2001 From: CodePanter Date: Tue, 5 May 2020 15:44:33 +0200 Subject: [PATCH 001/118] Standardized exception handling. --- uweb3/ext_lib/underdark/libs/sqltalk/sqlite/connection.py | 2 +- uweb3/pagemaker/admin.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/connection.py b/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/connection.py index 9611af50..740b4250 100644 --- a/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/connection.py +++ b/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/connection.py @@ -139,7 +139,7 @@ def run(self): response.put(SqliteResult(result.fetchall(), result.description, result.rowcount, result.lastrowid)) del execute, result - except Exception, error: + except Exception as error: response.put(error) del error diff --git a/uweb3/pagemaker/admin.py b/uweb3/pagemaker/admin.py index 5794dc06..163238c1 100644 --- a/uweb3/pagemaker/admin.py +++ b/uweb3/pagemaker/admin.py @@ -149,7 +149,7 @@ def __SaveRecord(self, table, key): obj[item] = int(self.post.getfirst(item, 0)) try: obj.Save() - except Exception, error: + except Exception as error: return error return 'Changes saved' return 'Invalid table' From dfabc1e7c50ea106175f6cf7303294d3ce0468f8 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 6 May 2020 09:05:11 +0200 Subject: [PATCH 002/118] Updated error message when there is no pagemaker/route handler found --- tables.sql | 5 ----- uweb3/__init__.py | 3 +-- 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/tables.sql b/tables.sql index f4c008e1..e69de29b 100644 --- a/tables.sql +++ b/tables.sql @@ -1,5 +0,0 @@ -CREATE TABLE users( - id INTEGER AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(255) NOT NULL, - password VARCHAR(255) NOT NULL -); \ No newline at end of file diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 95bbc51e..eb0de963 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -86,8 +86,7 @@ def router(self, routes): pattern(getattr(pagemaker, details[0])) continue if not pagemaker: - raise NoRouteError(f"""There is no handler called: {details[0]} in any of the projects PageMaker. - Static routes are automatically handled so there is no need to define them in routes anymore.""") + raise NoRouteError(f"µWeb3 could not find a route handler called '{details[0]}' in any of your projects PageMakers.") req_routes.append((re.compile(pattern + '$', re.UNICODE), details[0], #handler, details[1] if len(details) > 1 else 'ALL', #request types From d1abd4222936f1d843403cd56893eae7a6acdafa Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Tue, 12 May 2020 14:12:46 +0200 Subject: [PATCH 003/118] Removed old documentation from readme. Can now be found in uweb3 documentation --- README.md | 115 +--------------- uweb3/access_logging.log | 286 --------------------------------------- 2 files changed, 2 insertions(+), 399 deletions(-) delete mode 100644 uweb3/access_logging.log diff --git a/README.md b/README.md index 080dae25..2c9bfe87 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ This makes sure that µWeb3 restarts every time you modify something in the core µWeb3 has inbuild XSRF protection. You can import it from uweb3.pagemaker.new_decorators checkxsrf. This is a decorator and it will handle validation and generation of the XSRF. The only thing you have to do is add the ```{{ xsrf [xsrf]}}``` tag into a form. -The xsrf token is accessible in any pagemaker with self.xsrf. +The xsrf token is accessible in any pagemaker with self.xsrf. # Routing The default way to create new routes in µWeb3 is to create a folder called routes. @@ -108,115 +108,4 @@ After creating your pagemaker be sure to add the route endpoint to routes list i - A function called _TemplateConstructXsrf that generates a hidden input field with the supplied value: {{ xsrf [xsrf_variable]}} - In libs/sqltalk - Tried to make sqltalk python3 compatible by removing references to: long, unicode and basestring - - So far so good but it might crash on functions that I didn't use yet - - -# Login validation -Instead of using sessions to keep track of logged in users µWeb3 uses secure cookies. So how does this work? -When a user logs in for the first time there is no cookie in place, to set one we go through the normal process of validating a user and loggin in. - -To create a secure cookie inherit from the Model.SecureCookie. The SecureCookie class has a few build in methods, Create, Update and Delete. -To create a new cookie make use of the `Create` method, it works the same ass the AddCookie method. - -If you want to see which cookies are managed by the SecureCookie class you can call the session attribute. -The session attribute decodes all managed cookies and can be used to read them. - -# SQLAlchemy -SQLAlchemy is available in uWeb3 by using the SqAlchemyPageMaker instead of the regular pagemaker. -SQLAlchemy comes with most of the methods that are available in the default model.Record class, however because SQLAlchemy works like an ORM -there are some adjustments. Instead of inheriting from dict the SQLAlchemy model.Record inherits from object, meaning you can no longer use -dict like functions such as get and set. Instead the model is accessible by the columns defined in the class you want to create. - -The SQLAlchemy model.Record class makes use of the session attribute accessible in the SqAlchemyPageMaker. - -The session keeps track of all queries to the database and comes with some usefull features. - -An example of a few usefull features: -`session.new`: The set of all instances marked as ‘new’ within this Session. -`session.dirty`: Instances are considered dirty when they were modified but not deleted. -`session.deleted`: The set of all instances marked as ‘deleted’ within this Session -the rest can be found at https://docs.sqlalchemy.org/en/13/orm/session_api.html - -Objects in the session will only be updated/created in the actual database on session.commit()/session.flush(). - -Defining classes that represent a table is different from how we used to do it in uWeb2. -SQLAlchemy requires you to define all columns from the table that you want to use. -For example, creating a class that represents the user table could look like this: - -``` -from sqlalchemy import Column, Integer, String, ForeignKey -from sqlalchemy.ext.declarative import declarative_base - -Base = declarative_base() - -class User(Base): - __tablename__ = 'users' - - id = Column(Integer, primary_key=True) - username = Column(String, nullable=False, unique=True) - password = Column(String, nullable=False) -``` -We can now use this class to query our users table in the SqAlchemyPageMaker to get the user with id 1: -`self.session.query(User).filter(User.id == 1).first() ` -or to list all users: -`self.session.query(User).all()` -uWeb3's SQLAlchemy model.Record has almost the same functionality as uWeb3's regular model.Record so we can simplify our code to this: - -``` -from sqlalchemy import Column, Integer, String, ForeignKey -from sqlalchemy.ext.declarative import declarative_base - -Base = declarative_base() - -#Notice how we load in the uweb3.model.AlchemyRecord class to gain access to all sorts of functionality -class User(uweb3.model.AlchemyRecord, Base): - __tablename__ = 'users' - - id = Column(Integer, primary_key=True) - username = Column(String, nullable=False, unique=True) - password = Column(String, nullable=False) -``` -We can now query the users table like this: -``` -User.FromPrimary(self.session, 1) ->>> User({'id': 1, 'username': 'username', 'password': 'password'}) -``` -Or to get a list of all users: -``` -User.List(self.session, conditions=[User.id <= 2]) ->>> [ - User({'id': 1, 'username': 'name', 'password': 'password'}), - User({'id': 2, 'username': 'user2', 'password': 'password'}) - ] -``` - -Now if we want to automatically load related tables we can set it up like this: - -``` -from sqlalchemy import Column, Integer, String, ForeignKey -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, relationship - -Base = declarative_base() - -class User(uweb3.model.AlchemyRecord, Base): - __tablename__ = 'users' - - id = Column(Integer, primary_key=True) - username = Column(String, nullable=False, unique=True) - password = Column(String, nullable=False) - userinfoid = Column('userinfoid', Integer, ForeignKey('UserInfo.id')) - userdata = relationship("UserInfo", lazy="select") - - def __init__(self, *args, **kwargs): - super(User, self).__init__(*args, **kwargs) - -class UserInfo(uweb3.model.AlchemyRecord, Base): - __tablename__ = 'UserInfo' - - id = Column(Integer, primary_key=True) - name = Column(String, unique=True) -``` -Now the UserInfo table will be loaded on the `userinfoid` attribute, but only after we try and access -this key a seperate query is send to retrieve the related information. -SQLAlchemy's lazy loading is fast but should be avoided while in loops. Take a look at SQLAlchemys documentation for optimal use. + - So far so good but it might crash on functions that I didn't use yet \ No newline at end of file diff --git a/uweb3/access_logging.log b/uweb3/access_logging.log deleted file mode 100644 index 89c8f793..00000000 --- a/uweb3/access_logging.log +++ /dev/null @@ -1,286 +0,0 @@ -127.0.0.1 - - [29/04/2020 09:44:40] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:40] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:41] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:42] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:47] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:47] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:49] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:51] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:53] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:53] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:54] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:57] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:44:57] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:45:10] "GET /home 303 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:45:10] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:52:19] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:52:27] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:54:58] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:54:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:55:07] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:55:30] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:55:33] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:57:19] "GET / 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:57:20] "GET / 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:57:41] "GET / 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:57:42] "GET / 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:57:46] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:57:48] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:13] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:14] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:14] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:21] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:22] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:22] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:23] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:23] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:28] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:48] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:56] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:58:57] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:59:07] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:59:31] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 09:59:31] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:00:23] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:01:33] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:02:46] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:29:41] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:29:41] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:30:09] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:30:19] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:30:23] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:30:49] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:31:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:31:34] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:34:14] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:34:44] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:44:09] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:44:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:44:12] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:44:35] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:45:00] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:45:35] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:45:46] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:46:16] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:46:17] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:46:25] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:46:31] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 10:46:38] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:46:49] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:47:11] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:48:00] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:48:05] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:48:30] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:49:05] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:49:59] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:50:00] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:46] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:46] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:46] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:46] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:47] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:47] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:47] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:47] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:47] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:48] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:48] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:48] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:48] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:50] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:51] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:51] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:51] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:53:51] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:09] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:10] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:14] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:14] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:14] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:55:14] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:58:21] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:58:21] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 10:58:21] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:45] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:45] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:45] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:45] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:46] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:46] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:46] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:01:46] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:23] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:23] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:23] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:23] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:33] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:34] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:34] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:34] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:02:34] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:27] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:28] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:28] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:28] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:33] "POST /ULF-Login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:33] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:33] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:35] "POST /ULF-Login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:35] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:35] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:35] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:36] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:36] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:36] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:36] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:36] "GET /signup 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:36] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:36] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:37] "POST /ULF-Login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:37] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:37] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:37] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:42] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:42] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:42] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:42] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:45] "POST /ULF-Login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:45] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:45] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:45] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:49] "POST /ULF-Login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:49] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:49] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:54] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:54] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:54] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:54] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:56] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:56] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:56] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:56] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:57] "POST /ULF-Login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:57] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:57] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:03:57] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:04:02] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:04:02] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:04:02] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:06:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:06:42] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:06:43] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:07:46] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:07:47] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:07:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:07:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:08:45] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:08:48] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:11:56] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:12:22] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:12:25] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:12:26] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:12:36] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:13:06] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:15:02] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:15:03] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:16:02] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:19:05] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:06] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:06] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:06] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:30] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:30] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:30] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:30] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:32] "GET / 200 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:32] "GET /styles/default.css 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:32] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:19:32] "GET /js/login-fields.js 404 HTTP/1.1" -127.0.0.1 - - [29/04/2020 11:37:34] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:37:34] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:37:36] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:37:43] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:37:55] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:37:57] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 11:37:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:04:47] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:04:48] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:04:49] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:32:34] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:32:34] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:32:36] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:32:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:32:38] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:33:31] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:33:37] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:33:47] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:33:48] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:34:06] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:34:07] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:43:32] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:43:33] "GET / 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:44:13] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:44:14] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:44:19] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:45:26] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:45:46] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:45:48] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:47:18] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:47:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:48:00] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:49:45] "GET / 500 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:49:51] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:50:12] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:50:21] "GET / 200 HTTP/1.0" -127.0.0.1 - - [29/04/2020 12:52:00] "GET / 200 HTTP/1.0" From 5c8e1536acbadd47cbc8e0ca337896c16f2e3b35 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Tue, 12 May 2020 14:32:57 +0200 Subject: [PATCH 004/118] Added the unescape method for SQLSAFE --- .../underdark/libs/safestring/__init__.py | 65 +++++++++++++------ .../ext_lib/underdark/libs/safestring/test.py | 12 +++- uweb3/scaffold/routes/test.py | 7 +- 3 files changed, 61 insertions(+), 23 deletions(-) diff --git a/uweb3/ext_lib/underdark/libs/safestring/__init__.py b/uweb3/ext_lib/underdark/libs/safestring/__init__.py index ed32b72e..5f933ecf 100644 --- a/uweb3/ext_lib/underdark/libs/safestring/__init__.py +++ b/uweb3/ext_lib/underdark/libs/safestring/__init__.py @@ -21,7 +21,7 @@ """ #TODO: logger geen enters -#bash injection +#bash injection #mysql escaping __author__ = 'Jan Klopper (jan@underdark.nl)' __version__ = 0.1 @@ -84,18 +84,31 @@ def unescape(self, data): class SQLSAFE(Basesafestring): CHARS_ESCAPE_DICT = { - '\0' : '\\0', - '\b' : '\\b', - '\t' : '\\t', - '\n' : '\\n', - '\r' : '\\r', - '\x1a' : '\\Z', - '"' : '\\"', - '\'' : '\\\'', - '\\' : '\\\\' + '\0': '\\0', + '\b': '\\b', + '\t': '\\t', + '\n': '\\n', + '\r': '\\r', + '\x1a': '\\Z', + '"': '\\"', + '\'': '\\\'', + '\\': '\\\\' } - + + CHARS_UNESCAPE_DICT = { + '\\\\0': '\0', + '\\\\b': '\b', + '\\\\t': '\t', + '\\\\n': '\n', + '\\\\r': '\r', + '\\\\Z': '\x1a', + '\\\\"': '"', + '\\\\\'': '\'', + '\\\\\\': '\\' + } + CHARS_ESCAPE_REGEX = re.compile(r"""[\0\b\t\n\r\x1a\"\'\\]""") + CHARS_UNESCAPE_REGEX = re.compile(r"""((\\t)|(\\0)|(\\n)|(\\b)|(\\r)|(\\Z)|(\\")|(\\\\')|(\\\\\\))""") PLACEHOLDERS_REGEX = re.compile(r"""\?+""") QUOTES_REGEX = re.compile(r"""([\"'])(?:(?=(\\?))\2.)*?\1""", re.DOTALL) @@ -135,25 +148,39 @@ def sanitize(cls, value, qoutes=True): return escaped return escaped - def escape(cls, sql, values): + def escape(self, sql, values): x = 0 escaped = "" if not isinstance(values, tuple): raise ValueError("Values should be a tuple") - if len(cls.PLACEHOLDERS_REGEX.findall(sql)) != len(values): + if len(self.PLACEHOLDERS_REGEX.findall(sql)) != len(values): raise ValueError("Number of values does not match number of replacements") - for index, m in enumerate(cls.PLACEHOLDERS_REGEX.finditer(sql)): - escaped += sql[x:m.span()[0]] + cls.sanitize(values[index]) + + for index, m in enumerate(self.PLACEHOLDERS_REGEX.finditer(sql)): + escaped += sql[x:m.span()[0]] + self.sanitize(values[index]) x = m.span()[1] escaped += sql[x:] return SQLSAFE(escaped) - - + + def unescape(self, value): + if not isinstance(value, SQLSAFE): + raise ValueError(f"The value needs to be an instance of the SQLSAFE class and not of type: {type(value)}") + x = 0 + escaped = "" + for index, m in enumerate(self.CHARS_UNESCAPE_REGEX.finditer(value)): + escaped += value[x:m.span()[0] - 1] + target = value[m.span()[0] - 1:m.span()[1]] + escaped += self.CHARS_UNESCAPE_DICT.get(target) + x = m.span()[1] + escaped += value[x:] + return SQLSAFE(escaped) + + # what follows are the actual useable classes that are safe in specific contexts class HTMLsafestring(Basesafestring): """This class signals that the content is HTML safe""" - - + + def escape(self, data): return html.escape(data) diff --git a/uweb3/ext_lib/underdark/libs/safestring/test.py b/uweb3/ext_lib/underdark/libs/safestring/test.py index d0f36b8a..d024c8c6 100755 --- a/uweb3/ext_lib/underdark/libs/safestring/test.py +++ b/uweb3/ext_lib/underdark/libs/safestring/test.py @@ -107,7 +107,6 @@ def test_escaping(self): self.assertEqual(testdata, "SELECT * FROM users WHERE username = 'username\\\"'") testdata = SQLSAFE("""SELECT * FROM users WHERE username = ? AND ? """, values=('username"', "password"), unsafe=True) - def test_concatenation(self): testdata = SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=("username'",), unsafe=True) other = "AND firstname='test'" @@ -116,5 +115,16 @@ def test_concatenation(self): other = "AND firstname='test'" self.assertEqual(testdata + other, "SELECT * FROM users WHERE username = 'username\\\"' AND firstname=\\'test\\'") + # def test_unescape_wrong_type(self): + # """Validate if the string we are trying to unescape is part of an SQLSAFE instance""" + # testdata = SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=("username'",), unsafe=True) + # with self.assertRaises(ValueError) as msg: + # self.assertRaises(testdata.unescape('whatever')) + + def test_unescape(self): + testdata = SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=("username\\t \\0",), unsafe=True) + testdata.unescape(testdata) + + if __name__ == '__main__': unittest.main() diff --git a/uweb3/scaffold/routes/test.py b/uweb3/scaffold/routes/test.py index def875a0..0422d9f4 100644 --- a/uweb3/scaffold/routes/test.py +++ b/uweb3/scaffold/routes/test.py @@ -55,7 +55,8 @@ def Parsed(self): def StringEscaping(self): if self.post: - result = SQLSAFE(self.post.getfirst('sql'), self.post.getfirst('value1'), self.post.getfirst('value2'), unsafe=True) - t = f"""t = 't"''""" - print(result + t) + result = SQLSAFE(self.post.getfirst('sql'), values=(self.post.getfirst('value1'), self.post.getfirst('value2')), unsafe=True) + print(result) + print(result.unescape(result)) + return self.req.Redirect('/test') \ No newline at end of file From 53432fbde935a7cf9abe262da3b61f481c51757b Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 13 May 2020 11:03:46 +0200 Subject: [PATCH 005/118] Removed the underdark folder in ext_lib. Moved all files 1 folder back --- uweb3/ext_lib/{underdark => }/__init__.py | 0 uweb3/ext_lib/{underdark => }/libs/__init__.py | 0 uweb3/ext_lib/{underdark => }/libs/safestring/__init__.py | 0 uweb3/ext_lib/{underdark => }/libs/safestring/test.py | 2 +- uweb3/ext_lib/{underdark => }/libs/sqltalk/__init__.py | 0 uweb3/ext_lib/{underdark => }/libs/sqltalk/mysql/__init__.py | 0 .../ext_lib/{underdark => }/libs/sqltalk/mysql/connection.py | 0 uweb3/ext_lib/{underdark => }/libs/sqltalk/mysql/constants.py | 0 .../ext_lib/{underdark => }/libs/sqltalk/mysql/converters.py | 0 uweb3/ext_lib/{underdark => }/libs/sqltalk/mysql/cursor.py | 0 uweb3/ext_lib/{underdark => }/libs/sqltalk/mysql/times.py | 0 uweb3/ext_lib/{underdark => }/libs/sqltalk/sqlite/__init__.py | 0 .../ext_lib/{underdark => }/libs/sqltalk/sqlite/connection.py | 0 .../ext_lib/{underdark => }/libs/sqltalk/sqlite/converters.py | 0 uweb3/ext_lib/{underdark => }/libs/sqltalk/sqlite/cursor.py | 0 uweb3/ext_lib/{underdark => }/libs/sqltalk/sqlresult.py | 0 uweb3/ext_lib/{underdark => }/libs/sqltalk/sqlresult_test.py | 0 uweb3/ext_lib/{underdark => }/libs/urlsplitter/__init__.py | 0 uweb3/ext_lib/{underdark => }/libs/urlsplitter/test.py | 0 uweb3/scaffold/routes/test.py | 2 +- uweb3/templateparser.py | 2 +- uweb3/test_model.py | 4 ++-- uweb3/test_model_alchemy.py | 2 +- 23 files changed, 6 insertions(+), 6 deletions(-) rename uweb3/ext_lib/{underdark => }/__init__.py (100%) rename uweb3/ext_lib/{underdark => }/libs/__init__.py (100%) rename uweb3/ext_lib/{underdark => }/libs/safestring/__init__.py (100%) rename uweb3/ext_lib/{underdark => }/libs/safestring/test.py (96%) rename uweb3/ext_lib/{underdark => }/libs/sqltalk/__init__.py (100%) rename uweb3/ext_lib/{underdark => }/libs/sqltalk/mysql/__init__.py (100%) rename uweb3/ext_lib/{underdark => }/libs/sqltalk/mysql/connection.py (100%) rename uweb3/ext_lib/{underdark => }/libs/sqltalk/mysql/constants.py (100%) rename uweb3/ext_lib/{underdark => }/libs/sqltalk/mysql/converters.py (100%) rename uweb3/ext_lib/{underdark => }/libs/sqltalk/mysql/cursor.py (100%) rename uweb3/ext_lib/{underdark => }/libs/sqltalk/mysql/times.py (100%) rename uweb3/ext_lib/{underdark => }/libs/sqltalk/sqlite/__init__.py (100%) rename uweb3/ext_lib/{underdark => }/libs/sqltalk/sqlite/connection.py (100%) rename uweb3/ext_lib/{underdark => }/libs/sqltalk/sqlite/converters.py (100%) rename uweb3/ext_lib/{underdark => }/libs/sqltalk/sqlite/cursor.py (100%) rename uweb3/ext_lib/{underdark => }/libs/sqltalk/sqlresult.py (100%) rename uweb3/ext_lib/{underdark => }/libs/sqltalk/sqlresult_test.py (100%) rename uweb3/ext_lib/{underdark => }/libs/urlsplitter/__init__.py (100%) rename uweb3/ext_lib/{underdark => }/libs/urlsplitter/test.py (100%) diff --git a/uweb3/ext_lib/underdark/__init__.py b/uweb3/ext_lib/__init__.py similarity index 100% rename from uweb3/ext_lib/underdark/__init__.py rename to uweb3/ext_lib/__init__.py diff --git a/uweb3/ext_lib/underdark/libs/__init__.py b/uweb3/ext_lib/libs/__init__.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/__init__.py rename to uweb3/ext_lib/libs/__init__.py diff --git a/uweb3/ext_lib/underdark/libs/safestring/__init__.py b/uweb3/ext_lib/libs/safestring/__init__.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/safestring/__init__.py rename to uweb3/ext_lib/libs/safestring/__init__.py diff --git a/uweb3/ext_lib/underdark/libs/safestring/test.py b/uweb3/ext_lib/libs/safestring/test.py similarity index 96% rename from uweb3/ext_lib/underdark/libs/safestring/test.py rename to uweb3/ext_lib/libs/safestring/test.py index d024c8c6..89d8b833 100755 --- a/uweb3/ext_lib/underdark/libs/safestring/test.py +++ b/uweb3/ext_lib/libs/safestring/test.py @@ -7,7 +7,7 @@ import unittest #custom modules -from uweb3.ext_lib.underdark.libs.safestring import URLsafestring, SQLSAFE, HTMLsafestring, URLqueryargumentsafestring, JSONsafestring, EmailAddresssafestring, Basesafestring +from uweb3.ext_lib.libs.safestring import URLsafestring, SQLSAFE, HTMLsafestring, URLqueryargumentsafestring, JSONsafestring, EmailAddresssafestring, Basesafestring class BasesafestringMethods(unittest.TestCase): def test_creation_str(self): diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/__init__.py b/uweb3/ext_lib/libs/sqltalk/__init__.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/sqltalk/__init__.py rename to uweb3/ext_lib/libs/sqltalk/__init__.py diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/mysql/__init__.py b/uweb3/ext_lib/libs/sqltalk/mysql/__init__.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/sqltalk/mysql/__init__.py rename to uweb3/ext_lib/libs/sqltalk/mysql/__init__.py diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/mysql/connection.py b/uweb3/ext_lib/libs/sqltalk/mysql/connection.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/sqltalk/mysql/connection.py rename to uweb3/ext_lib/libs/sqltalk/mysql/connection.py diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/mysql/constants.py b/uweb3/ext_lib/libs/sqltalk/mysql/constants.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/sqltalk/mysql/constants.py rename to uweb3/ext_lib/libs/sqltalk/mysql/constants.py diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/mysql/converters.py b/uweb3/ext_lib/libs/sqltalk/mysql/converters.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/sqltalk/mysql/converters.py rename to uweb3/ext_lib/libs/sqltalk/mysql/converters.py diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/mysql/cursor.py b/uweb3/ext_lib/libs/sqltalk/mysql/cursor.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/sqltalk/mysql/cursor.py rename to uweb3/ext_lib/libs/sqltalk/mysql/cursor.py diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/mysql/times.py b/uweb3/ext_lib/libs/sqltalk/mysql/times.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/sqltalk/mysql/times.py rename to uweb3/ext_lib/libs/sqltalk/mysql/times.py diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/__init__.py b/uweb3/ext_lib/libs/sqltalk/sqlite/__init__.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/sqltalk/sqlite/__init__.py rename to uweb3/ext_lib/libs/sqltalk/sqlite/__init__.py diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/connection.py b/uweb3/ext_lib/libs/sqltalk/sqlite/connection.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/sqltalk/sqlite/connection.py rename to uweb3/ext_lib/libs/sqltalk/sqlite/connection.py diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/converters.py b/uweb3/ext_lib/libs/sqltalk/sqlite/converters.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/sqltalk/sqlite/converters.py rename to uweb3/ext_lib/libs/sqltalk/sqlite/converters.py diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/sqlite/cursor.py b/uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/sqltalk/sqlite/cursor.py rename to uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/sqlresult.py b/uweb3/ext_lib/libs/sqltalk/sqlresult.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/sqltalk/sqlresult.py rename to uweb3/ext_lib/libs/sqltalk/sqlresult.py diff --git a/uweb3/ext_lib/underdark/libs/sqltalk/sqlresult_test.py b/uweb3/ext_lib/libs/sqltalk/sqlresult_test.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/sqltalk/sqlresult_test.py rename to uweb3/ext_lib/libs/sqltalk/sqlresult_test.py diff --git a/uweb3/ext_lib/underdark/libs/urlsplitter/__init__.py b/uweb3/ext_lib/libs/urlsplitter/__init__.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/urlsplitter/__init__.py rename to uweb3/ext_lib/libs/urlsplitter/__init__.py diff --git a/uweb3/ext_lib/underdark/libs/urlsplitter/test.py b/uweb3/ext_lib/libs/urlsplitter/test.py similarity index 100% rename from uweb3/ext_lib/underdark/libs/urlsplitter/test.py rename to uweb3/ext_lib/libs/urlsplitter/test.py diff --git a/uweb3/scaffold/routes/test.py b/uweb3/scaffold/routes/test.py index 0422d9f4..23c49b9c 100644 --- a/uweb3/scaffold/routes/test.py +++ b/uweb3/scaffold/routes/test.py @@ -6,7 +6,7 @@ from uweb3 import PageMaker from uweb3.pagemaker.new_login import UserCookie from uweb3.pagemaker.new_decorators import loggedin, checkxsrf -from uweb3.ext_lib.underdark.libs.safestring import SQLSAFE, HTMLsafestring +from uweb3.ext_lib.libs.safestring import SQLSAFE, HTMLsafestring from uweb3.model import SettingsManager class Test(PageMaker): diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index 9d245e37..7a2c75e6 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -17,7 +17,7 @@ import os import re import urllib.parse as urlparse -from .ext_lib.underdark.libs.safestring import * +from .ext_lib.libs.safestring import * import hashlib import itertools diff --git a/uweb3/test_model.py b/uweb3/test_model.py index cdd8a9ec..75f08cb4 100644 --- a/uweb3/test_model.py +++ b/uweb3/test_model.py @@ -8,7 +8,7 @@ import unittest # Importing uWeb3 makes the SQLTalk library available as a side-effect -from uweb3.ext_lib.underdark.libs.sqltalk import mysql +from uweb3.ext_lib.libs.sqltalk import mysql # Unittest target from uweb3 import model from pymysql.err import InternalError @@ -435,7 +435,7 @@ def DatabaseConnection(): passwd='24192419', db='uweb_test', charset='utf8') - + if __name__ == '__main__': diff --git a/uweb3/test_model_alchemy.py b/uweb3/test_model_alchemy.py index 3923e23e..5f7ecb6c 100644 --- a/uweb3/test_model_alchemy.py +++ b/uweb3/test_model_alchemy.py @@ -19,7 +19,7 @@ import uweb3 from uweb3.alchemy_model import AlchemyRecord -from uweb3.ext_lib.underdark.libs.sqltalk import mysql +from uweb3.ext_lib.libs.sqltalk import mysql # ############################################################################## # Record classes for testing From b7c555d4f6a1a49f525f50122d7b650c5f6a7b8f Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 13 May 2020 11:21:07 +0200 Subject: [PATCH 006/118] Added more unittests for SQLSAFE --- uweb3/ext_lib/libs/safestring/__init__.py | 30 +++++++++++------------ uweb3/ext_lib/libs/safestring/test.py | 28 +++++++++++++++------ 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/uweb3/ext_lib/libs/safestring/__init__.py b/uweb3/ext_lib/libs/safestring/__init__.py index 5f933ecf..e74f52ba 100644 --- a/uweb3/ext_lib/libs/safestring/__init__.py +++ b/uweb3/ext_lib/libs/safestring/__init__.py @@ -96,15 +96,15 @@ class SQLSAFE(Basesafestring): } CHARS_UNESCAPE_DICT = { - '\\\\0': '\0', - '\\\\b': '\b', - '\\\\t': '\t', - '\\\\n': '\n', - '\\\\r': '\r', - '\\\\Z': '\x1a', - '\\\\"': '"', - '\\\\\'': '\'', - '\\\\\\': '\\' + '\\0': '\0', + '\\b': '\b', + '\\t': '\t', + '\\n': '\n', + '\\r': '\r', + '\\Z': '\x1a', + '\\"': '"', + '\\\'': '\'', + '\\\\': '\\' } CHARS_ESCAPE_REGEX = re.compile(r"""[\0\b\t\n\r\x1a\"\'\\]""") @@ -126,15 +126,15 @@ def __upgrade__(self, other): return self.sanitize(otherdata) # escape it using our context else: other = " " + other - return self.sanitize(other, qoutes=False) + return self.sanitize(other, with_quotes=False) @classmethod - def sanitize(cls, value, qoutes=True): + def sanitize(cls, value, with_quotes=True): index = 0 escaped = "" if len(cls.CHARS_ESCAPE_REGEX.findall(value)) == 0: if not str.isdigit(value): - if qoutes: + if with_quotes: return f"'{value}'" return value return value @@ -143,7 +143,7 @@ def sanitize(cls, value, qoutes=True): index = m.span()[1] escaped += value[index:] if not str.isdigit(escaped): - if qoutes: + if with_quotes: return f"'{escaped}'" return escaped return escaped @@ -168,8 +168,8 @@ def unescape(self, value): x = 0 escaped = "" for index, m in enumerate(self.CHARS_UNESCAPE_REGEX.finditer(value)): - escaped += value[x:m.span()[0] - 1] - target = value[m.span()[0] - 1:m.span()[1]] + escaped += value[x:m.span()[0]] + target = value[m.span()[0]:m.span()[1]] escaped += self.CHARS_UNESCAPE_DICT.get(target) x = m.span()[1] escaped += value[x:] diff --git a/uweb3/ext_lib/libs/safestring/test.py b/uweb3/ext_lib/libs/safestring/test.py index 89d8b833..548b6689 100755 --- a/uweb3/ext_lib/libs/safestring/test.py +++ b/uweb3/ext_lib/libs/safestring/test.py @@ -115,15 +115,29 @@ def test_concatenation(self): other = "AND firstname='test'" self.assertEqual(testdata + other, "SELECT * FROM users WHERE username = 'username\\\"' AND firstname=\\'test\\'") - # def test_unescape_wrong_type(self): - # """Validate if the string we are trying to unescape is part of an SQLSAFE instance""" - # testdata = SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=("username'",), unsafe=True) - # with self.assertRaises(ValueError) as msg: - # self.assertRaises(testdata.unescape('whatever')) + def test_unescape_wrong_type(self): + """Validate if the string we are trying to unescape is part of an SQLSAFE instance""" + testdata = SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=("username'",), unsafe=True) + with self.assertRaises(ValueError) as msg: + self.assertRaises(testdata.unescape('whatever')) + + def test_correct_escape_character(self): + """Validate that all characters are escaped as expected""" + self.assertEqual(SQLSAFE.sanitize('\0', with_quotes=False), '\\0') + self.assertEqual(SQLSAFE.sanitize('\b', with_quotes=False), '\\b') + self.assertEqual(SQLSAFE.sanitize('\t', with_quotes=False), '\\t') + self.assertEqual(SQLSAFE.sanitize('\n', with_quotes=False), '\\n') + self.assertEqual(SQLSAFE.sanitize('\r', with_quotes=False), '\\r') + self.assertEqual(SQLSAFE.sanitize('\x1a', with_quotes=False), '\\Z') + self.assertEqual(SQLSAFE.sanitize('"', with_quotes=False), '\\"') + self.assertEqual(SQLSAFE.sanitize('\'', with_quotes=False), '\\\'') + self.assertEqual(SQLSAFE.sanitize('\\', with_quotes=False), '\\\\') def test_unescape(self): - testdata = SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=("username\\t \\0",), unsafe=True) - testdata.unescape(testdata) + """Validate that the string is converted back to the original after escaping and unescaping""" + testdata = SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=("username\t \t",), unsafe=True) + self.assertEqual(testdata, "SELECT * FROM users WHERE username = 'username\\t \\t'") + self.assertEqual(testdata.unescape(testdata), "SELECT * FROM users WHERE username = 'username\t \t'") if __name__ == '__main__': From f4331e8384fca67605b2141b704d16e98b3614bb Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 13 May 2020 11:32:26 +0200 Subject: [PATCH 007/118] More SQLSAFE unittests --- uweb3/ext_lib/libs/safestring/__init__.py | 1 + uweb3/ext_lib/libs/safestring/test.py | 16 +++++++++++++++- 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/uweb3/ext_lib/libs/safestring/__init__.py b/uweb3/ext_lib/libs/safestring/__init__.py index e74f52ba..db997e90 100644 --- a/uweb3/ext_lib/libs/safestring/__init__.py +++ b/uweb3/ext_lib/libs/safestring/__init__.py @@ -153,6 +153,7 @@ def escape(self, sql, values): escaped = "" if not isinstance(values, tuple): raise ValueError("Values should be a tuple") + if len(self.PLACEHOLDERS_REGEX.findall(sql)) != len(values): raise ValueError("Number of values does not match number of replacements") diff --git a/uweb3/ext_lib/libs/safestring/test.py b/uweb3/ext_lib/libs/safestring/test.py index 548b6689..517b8a74 100755 --- a/uweb3/ext_lib/libs/safestring/test.py +++ b/uweb3/ext_lib/libs/safestring/test.py @@ -100,6 +100,20 @@ def test_unsafe_init(self): self.assertEqual(testdata, 'jan@underdark.nl') class TestSQLSAFEMethods(unittest.TestCase): + def test_user_supplied_safe_value(self): + user_supplied_safe_object = SQLSAFE("SELECT * FROM users WHERE username = 'username\t'") + self.assertEqual(user_supplied_safe_object, "SELECT * FROM users WHERE username = 'username\t'") + self.assertIsInstance(user_supplied_safe_object, SQLSAFE) + + def test_escaping_wrong_values_type(self): + with self.assertRaises(ValueError): + self.assertRaises(SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=["username'"], unsafe=True)) + + def test_escaping_uneven_replacements_and_values(self): + with self.assertRaises(ValueError): + self.assertRaises(SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=["username'", "test"], unsafe=True)) + self.assertRaises(SQLSAFE("""SELECT * FROM users WHERE username = ? AND name=?""", values=["username'"], unsafe=True)) + def test_escaping(self): testdata = SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=("username'",), unsafe=True) self.assertEqual(testdata, "SELECT * FROM users WHERE username = 'username\\''") @@ -118,7 +132,7 @@ def test_concatenation(self): def test_unescape_wrong_type(self): """Validate if the string we are trying to unescape is part of an SQLSAFE instance""" testdata = SQLSAFE("""SELECT * FROM users WHERE username = ?""", values=("username'",), unsafe=True) - with self.assertRaises(ValueError) as msg: + with self.assertRaises(ValueError): self.assertRaises(testdata.unescape('whatever')) def test_correct_escape_character(self): From caf9c5cb3186ccb76272be36e138ac10f34393a2 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 13 May 2020 11:39:00 +0200 Subject: [PATCH 008/118] Removed OpenID stuff from uWeb3. --- uweb3/__init__.py | 1 - uweb3/helpers.py | 1 - uweb3/pagemaker/login.py | 123 - uweb3/pagemaker/login_openid.py | 178 - uweb3/request.py | 1 - uweb3/response.py | 15 +- uweb3/scaffold/access_logging.log | 983 ------ uweb3/scaffold/uweb3_uncaught_exceptions.log | 3296 ------------------ 8 files changed, 1 insertion(+), 4597 deletions(-) delete mode 100644 uweb3/pagemaker/login_openid.py delete mode 100644 uweb3/scaffold/access_logging.log delete mode 100644 uweb3/scaffold/uweb3_uncaught_exceptions.log diff --git a/uweb3/__init__.py b/uweb3/__init__.py index eb0de963..a1b04dc1 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -29,7 +29,6 @@ # Package classes from .response import Response -from .response import Redirect from .pagemaker import PageMaker from .pagemaker import DebuggingPageMaker from .pagemaker import SqAlchemyPageMaker diff --git a/uweb3/helpers.py b/uweb3/helpers.py index f64d75d6..09fd81b3 100644 --- a/uweb3/helpers.py +++ b/uweb3/helpers.py @@ -135,4 +135,3 @@ def handle(self, env, start_response, filename): else: return http404(env, start_response) - \ No newline at end of file diff --git a/uweb3/pagemaker/login.py b/uweb3/pagemaker/login.py index bd1f82e0..249f8216 100644 --- a/uweb3/pagemaker/login.py +++ b/uweb3/pagemaker/login.py @@ -10,42 +10,9 @@ import os import base64 -# Third-party modules -import simplejson - # Package modules from uweb3.model import SecureCookie -from . import login_openid from .. import model -from .. import response - -OPENID_PROVIDERS = {'google': 'https://www.google.com/accounts/o8/id', - 'yahoo': 'http://yahoo.com/', - 'myopenid': 'http://myopenid.com/'} - - -# ############################################################################## -# Record classes for Underdark Login Framework -# -class Challenge(model.Record): - """Abstraction for the `challenge` table.""" - _PRIMARY_KEY = 'user', 'remote' - CHALLENGE_BYTES = 16 - - @classmethod - def ChallengeBytes(cls): - """Returns the configured number of random bytes for a challenge.""" - return os.urandom(cls.CHALLENGE_BYTES) - - @classmethod - def MakeChallenge(cls, connection, remote, user): - """Makes a new, or retrieves an existing challenge for a given IP + user.""" - record = {'remote': remote, 'user': user, 'challenge': cls.ChallengeBytes()} - try: - return super(Challenge, cls).Create(connection, record) - except connection.IntegrityError: - return cls.FromPrimary(connection, (user, remote)) - class User(model.Record): """Abstraction for the `user` table.""" @@ -101,93 +68,3 @@ def VerifyPlaintext(self, plaintext): salted = self.HashPassword(plaintext, self['salt'].encode('utf-8'))['password'] return salted == self['password'] - -# ############################################################################## -# Actual Pagemaker mixin class -# -class LoginMixin(SecureCookie): - """Provides the Login Framework for uWeb3.""" - ULF_CHALLENGE = Challenge - ULF_USER = User - - def ValidateLogin(self): - user = self.ULF_USER.FromName( - self.connection, self.post.getfirst('username')) - if user.VerifyPlaintext(str(self.post.getfirst('password', ''))): - return self._Login_Success(user) - return self._Login_Failure() - - def _Login_Success(self, user): - """Renders the response to the user upon authentication failure.""" - raise NotImplementedError - - def _ULF_Success(self, secure): - """Renders the response to the user upon authentication success.""" - raise NotImplementedError - - -class OpenIdMixin(object): - """A class that provides rudimentary OpenID authentication. - - At present, it does not support any means of Attribute Exchange (AX) or other - account information requests (sReg). However, it does provide the base - necessities for verifying that whoever logs in is still the same person as the - one that was previously registered. - """ - def _OpenIdInitiate(self, provider=None): - """Verifies the supplied OpenID URL and resolves a login through it.""" - if provider: - try: - openid_url = OPENID_PROVIDERS[provider.lower()] - except KeyError: - return self.OpenIdProviderError('Invalid OpenID provider %r' % provider) - else: - openid_url = self.post.getfirst('openid_provider') - - consumer = login_openid.OpenId(self.req) - # set the realm that we want to ask to user to verify to - trustroot = 'http://%s' % self.req.env['HTTP_HOST'] - # set the return url that handles the validation - returnurl = trustroot + '/OpenIDValidate' - - try: - return consumer.Verify(openid_url, trustroot, returnurl) - except login_openid.InvalidOpenIdUrl as error: - return self.OpenIdProviderBadLink(error) - except login_openid.InvalidOpenIdService as error: - return self.OpenIdProviderError(error) - - def _OpenIdValidate(self): - """Handles the return url that openId uses to send the user to""" - try: - auth_dict = login_openid.OpenId(self.req).doProcess() - except login_openid.VerificationFailed as error: - return self.OpenIdAuthFailure(error) - except login_openid.VerificationCanceled as error: - return self.OpenIdAuthCancel(error) - return self.OpenIdAuthSuccess(auth_dict) - - def OpenIdProviderBadLink(self, err_obj): - """Handles the case where the OpenID provider link is faulty.""" - raise NotImplementedError - - def OpenIdProviderError(self, err_obj): - """Handles the case where the OpenID provider responds out of spec.""" - raise NotImplementedError - - def OpenIdAuthCancel(self, err_obj): - """Handles the case where the client cancels OpenID authentication.""" - raise NotImplementedError - - def OpenIdAuthFailure(self, err_obj): - """Handles the case where the provided authentication is invalid.""" - raise NotImplementedError - - def OpenIdAuthSuccess(self, auth_dict): - """Handles the case where the OpenID authentication was successful. - - Implementers should at the very least override this method as this is where - you will want to mark people as authenticated, either by cookies or sessions - tracked otherwise. - """ - raise NotImplementedError diff --git a/uweb3/pagemaker/login_openid.py b/uweb3/pagemaker/login_openid.py deleted file mode 100644 index fb80bb4a..00000000 --- a/uweb3/pagemaker/login_openid.py +++ /dev/null @@ -1,178 +0,0 @@ -#!/usr/bin/python3 -"""module to support OpenID login in uWeb3""" - -# Standard modules -import base64 -import os -from openid.consumer import consumer -from openid.extensions import pape -from openid.extensions import sreg - -# Package modules -from . import response - - -class Error(Exception): - """An OpenID error has occured""" - - -class InvalidOpenIdUrl(Error): - """The supplied openIDurl is invalid""" - - -class InvalidOpenIdService(Error): - """The supplied openID Service is invalid""" - - -class VerificationFailed(Error): - """The verification for the user failed""" - - -class VerificationCanceled(Error): - """The verification for the user was canceled""" - - -class OpenId(object): - """Provides OpenId verification and processing of return values""" - def __init__(self, request, cookiename='nw_openid'): - """Sets up the openId class - - Arguments: - @ request: request.Request - The request object. - % cookiename: str ~~ 'nw_openid' - The name of the cookie that holds the OpenID session token. - """ - self.request = request - self.session = {'id': None} - self.cookiename = cookiename - - def getConsumer(self): - """Creates a openId consumer class and returns it""" - #XXX(Elmer): What does having a store change? - # As far as I can tell, this does *not* maintain sessions of any sort. - store = None - return consumer.Consumer(self.getSession(), store) - - def getSession(self): - """Return the existing session or a new session""" - if self.session['id'] is not None: - return self.session - - # Get value of cookie header that was sent - try: - self.session['id'] = self.request.vars['cookies'][self.cookiename].value - except KeyError: - # 20 chars long, 120 bits of entropy - self.session['id'] = base64.urlsafe_b64encode(os.urandom(15)) - - return self.session - - def setSessionCookie(self): - """Sets the session cookie on the request object""" - self.request.AddCookie(self.cookiename, self.session['id']) - - def Verify(self, openid_url, trustroot, returnurl): - """ - Takes the openIdUrl from the user and sets up the request to send the user - to the correct page that will validate our trustroot to receive the data. - - Arguments: - @ openid_url: str - The supplied URL where the OpenID provider lives. - @ trustroot: str - The url of our webservice, will be displayed to the user as th - consuming url - @ returnurl: str - The url that will handle the Process step for the user being returned - to us by the openId supplier - """ - oidconsumer = self.getConsumer() - if openid_url.strip() == '': - raise InvalidOpenIdService() - try: - request = oidconsumer.begin(openid_url) - except consumer.DiscoveryFailure: - raise InvalidOpenIdUrl(openid_url) - if not request: - raise InvalidOpenIdService() - if request.shouldSendRedirect(): - redirect_url = request.redirectURL(trustroot, returnurl) - return response.Redirect(redirect_url) - else: - return request.htmlMarkup(trustroot, returnurl, - form_tag_attrs={'id': 'openid_message'}) - - def doProcess(self): - """Handle the redirect from the OpenID server. - - Returns: - tuple: userId - requested fields - phishing resistant info - canonical user ID - - Raises: - VerificationCanceled if the user canceled the verification - VerificationFailed if the verification failed - """ - oidconsumer = self.getConsumer() - - # Ask the library to check the response that the server sent - # us. Status is a code indicating the response type. info is - # either None or a string containing more information about - # the return type. - url = 'http://%s%s' % ( - self.request.env['HTTP_HOST'], self.request.env['PATH_INFO']) - query_args = dict((key, value[0]) for key, value - in self.request.vars['get'].items()) - info = oidconsumer.complete(query_args, url) - - sreg_resp = None - pape_resp = None - display_identifier = info.getDisplayIdentifier() - - if info.status == consumer.FAILURE and display_identifier: - # In the case of failure, if info is non-None, it is the - # URL that we were verifying. We include it in the error - # message to help the user figure out what happened. - raise VerificationFailed('Verification of %s failed: %s' % ( - display_identifier, info.message)) - - elif info.status == consumer.SUCCESS: - # Success means that the transaction completed without - # error. If info is None, it means that the user cancelled - # the verification. - - # This is a successful verification attempt. If this - # was a real application, we would do our login, - # comment posting, etc. here. - sreg_resp = sreg.SRegResponse.fromSuccessResponse(info) - pape_resp = pape.Response.fromSuccessResponse(info) - # You should authorize i-name users by their canonicalID, - # rather than their more human-friendly identifiers. That - # way their account with you is not compromised if their - # i-name registration expires and is bought by someone else. - return {'ident': display_identifier, - 'sreg': sreg_resp, - 'pape': pape_resp, - 'canonicalID': info.endpoint.canonicalID} - - elif info.status == consumer.CANCEL: - # cancelled - raise VerificationCanceled('Verification canceled') - - elif info.status == consumer.SETUP_NEEDED: - if info.setup_url: - message = 'Setup needed' % info.setup_url - else: - # This means auth didn't succeed, but you're welcome to try - # non-immediate mode. - message = 'Setup needed' - raise VerificationFailed(message) - else: - # Either we don't understand the code or there is no - # openid_url included with the error. Give a generic - # failure message. The library should supply debug - # information in a log. - raise VerificationFailed('Verification failed.') diff --git a/uweb3/request.py b/uweb3/request.py index 17711327..7fd57e40 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -90,7 +90,6 @@ def __init__(self, env, registry): self._out_status = 200 self._response = None self.method = self.env['REQUEST_METHOD'] - # `self.vars` setup, will contain keys 'cookie', 'get' and 'post' self.vars = {'cookie': dict((name, value.value) for name, value in Cookie(self.env.get('HTTP_COOKIE')).items()), 'get': PostDictionary(cgi.parse_qs(self.env.get('QUERY_STRING'))), diff --git a/uweb3/response.py b/uweb3/response.py index 306f15ef..c0640e7f 100644 --- a/uweb3/response.py +++ b/uweb3/response.py @@ -75,7 +75,7 @@ def headerlist(self): val = str(val) tuple_list.append((key, val.encode('ascii', 'ignore').decode('ascii'))) return tuple_list - + @property def status(self): if not self.httpcode: @@ -88,16 +88,3 @@ def __repr__(self): def __str__(self): return self.content - -class Redirect(Response): - """A response tailored to do redirects.""" - REDIRECT_PAGE = ('Page moved' - 'Page moved, please follow this link' - '') - #TODO make sure we inject cookies set on the previous response by copying any Set-Cookie headers from them into these headers. - def __init__(self, location, httpcode=307): - super(Redirect, self).__init__( - content=self.REDIRECT_PAGE % location, - content_type='text/html', - httpcode=httpcode, - headers={'Location': location}) diff --git a/uweb3/scaffold/access_logging.log b/uweb3/scaffold/access_logging.log deleted file mode 100644 index b43e3662..00000000 --- a/uweb3/scaffold/access_logging.log +++ /dev/null @@ -1,983 +0,0 @@ -127.0.0.1 - - [21/04/2020 10:43:34] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:11:23] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:11:23] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:11:26] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:11:29] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:11:30] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:11:39] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:13:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:15:49] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:21:53] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:34:31] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:34:33] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:34:34] "POST /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:35:41] "POST /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:35:50] "POST /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:37:36] "POST /login 303 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:37:36] "GET /home 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:37:49] "GET /home 303 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:37:49] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:37:50] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:38:37] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:38:38] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:38:39] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:00] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:39:20] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:40:35] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:40:41] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:40:41] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:42:16] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:42:18] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:42:19] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:42:33] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:43:41] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:45:19] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:45:25] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:45:28] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:45:57] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:46:14] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:46:21] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:46:29] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:47:08] "GET /login 500 HTTP/1.1" -127.0.0.1 - - [21/04/2020 11:49:04] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:31:30] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:32:31] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:32:37] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:32:43] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:33:00] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:33:27] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:34:58] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:34:58] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:38:06] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:42:10] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:44:24] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:44:24] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:44:26] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:44:40] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:44:57] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:45:45] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:45:50] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:46:49] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:48:45] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:48:45] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:49:39] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:50:35] "POST /login 303 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:50:35] "GET /home 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:51:31] "GET /home 303 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:51:32] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:52:00] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:52:21] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:52:59] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:53:12] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:53:36] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:53:49] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:54:03] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:54:09] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:54:17] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:54:22] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:54:28] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:54:56] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:55:03] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:55:04] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:55:05] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:55:06] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:56:17] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:56:19] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:56:20] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:56:21] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:56:33] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:56:35] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:57:33] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:57:35] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:57:51] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:57:55] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:57:56] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:58:07] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:58:11] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:58:16] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 12:59:20] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:01:03] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:01:17] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:02:01] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:02:02] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:04:22] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:04:22] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:05:18] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:05:42] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:05:50] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:05:51] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:05:57] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:06:02] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:06:05] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:06:21] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:06:24] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:07:43] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:08:06] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:08:19] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:08:36] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:08:38] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:08:44] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:08:51] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:08:55] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:09:59] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:10:05] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:10:41] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:10:56] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:10:57] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:11:42] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:11:44] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:11:53] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:11:55] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:12:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:12:58] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:13:18] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:13:34] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:13:49] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:13:56] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:14:04] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:14:14] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:14:20] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:14:22] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:14:44] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:14:55] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:15:10] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:15:12] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:15:38] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:15:47] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:16:27] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:16:30] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:16:37] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:16:48] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:16:56] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:17:40] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:17:57] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:15] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:30] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:34] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:37] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:37] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:39] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:46] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:19:58] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:20:15] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:21:13] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:21:15] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:21:32] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:21:35] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:21:58] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:22:21] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:22:29] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:22:54] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:22:56] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:23:03] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:23:37] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:23:39] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:24:03] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:24:07] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:24:31] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:24:32] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:24:53] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:26:50] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:26:50] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:26:54] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:36:46] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:19] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:20] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:23] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:24] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:34] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:39] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:41] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:50] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:51] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:56] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:37:57] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:38:46] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:40:44] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 13:40:44] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [21/04/2020 14:15:43] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 14:34:49] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 14:34:51] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 14:34:52] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 14:35:44] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 14:35:45] "GET / 200 HTTP/1.1" -127.0.0.1 - - [21/04/2020 14:35:46] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 09:18:11] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 09:18:11] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [22/04/2020 09:18:14] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 09:18:28] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 09:18:31] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 10:27:25] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 10:27:25] "GET /favicon.ico 404 HTTP/1.1" -127.0.0.1 - - [22/04/2020 10:27:26] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 10:27:27] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 10:27:44] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 10:27:44] "GET / 200 HTTP/1.1" -127.0.0.1 - - [22/04/2020 11:15:18] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:18] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:18] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:18] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:18] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:20] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:20] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:20] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:20] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:20] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:25] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:25] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:25] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:25] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:25] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:35] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:35] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:35] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:35] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:35] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:39] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:39] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:39] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:39] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:39] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:41] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:41] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:41] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:41] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:41] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:41] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:53] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:53] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:53] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:53] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:53] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:56] "GET /static 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:15:58] "GET /static/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:18:26] "GET /static/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /test 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /static/scripts/ajax.js 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /static/scripts/uweb-dynamic.js 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /static/scripts/uweb3-template-parser.js 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:00] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:19:33] "GET /static/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:21:04] "GET /static/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:21:07] "GET /static 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:21:09] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:21:09] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:21:09] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:21:09] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:21:09] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:24:10] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:24:11] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:24:13] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:24:13] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:24:14] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:24:14] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:25:38] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:27:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:27:31] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:27:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:27:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:28:31] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:28:32] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:08] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:09] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:09] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:09] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:10] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:10] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:11] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:12] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:14] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:19] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:19] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:19] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:21] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:23] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:26] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:30] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:31] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:31] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:32] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:32] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:33] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:34] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:37] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:41] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:46] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:51] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:29:56] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:01] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:06] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:11] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:16] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:21] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:26] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:30] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:30] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:32] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:32] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:32] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:33] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:35] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:39] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:44] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:49] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:30:54] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:31:00] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:31:06] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:31:12] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:31:18] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:31:24] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:31:30] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:31:36] "GET /socket.io/ 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:35:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:35:29] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:35:34] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:35:40] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:35:40] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:35:41] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:35:41] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:36:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:37:04] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:37:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:59:36] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:59:36] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:59:37] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:59:38] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:59:51] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 11:59:52] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:25] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:25] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:25] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:25] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:25] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:26] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:26] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:26] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:26] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:26] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:33] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:33] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:33] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:33] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:33] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:34] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:34] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:34] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:34] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:34] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:36] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:36] "GET /static/css/base.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:36] "GET /static/css/theme.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:36] "GET /static/css/module.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:36] "GET /static/css/layout.css 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:53] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:54] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:54] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:55] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:00:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:01:54] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:02:13] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:02:14] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:02:46] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:03:29] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:11:16] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:11:17] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:11:21] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:11:21] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:11:21] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:11:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:14:39] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:57:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:57:57] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:57:58] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:57:58] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:57:58] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:58:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:59:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 12:59:45] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:00:34] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:00:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:00:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:00:44] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:01:15] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:01:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:01:34] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:01:40] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:01:41] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:01:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:02:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:09:58] "GET / 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:09:59] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:11:27] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:11:37] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:12:20] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:32:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:32:50] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:32:52] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:32:52] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:32:52] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:32:52] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:34:20] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:34:40] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:35:03] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:35:20] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:35:38] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:35:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:38:53] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:38:53] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:38:56] "GET /home/kappa 404 HTTP/1.0" -127.0.0.1 - - [22/04/2020 13:38:58] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:24:48] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:24:49] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:24:51] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:25:04] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:25:05] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:27:00] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:27:01] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:27:03] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:35:42] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:35:44] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:35:45] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:35:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:29] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:31] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:31] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:32] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:32] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:33] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:36:33] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:37:16] "GET /home 303 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:37:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:44:41] "GET / 200 HTTP/1.1" -127.0.0.1 - - [23/04/2020 09:44:43] "GET /login 200 HTTP/1.1" -127.0.0.1 - - [23/04/2020 09:44:43] "GET /socket.io/ 404 HTTP/1.1" -127.0.0.1 - - [23/04/2020 09:44:43] "GET /socket.io/ 404 HTTP/1.1" -127.0.0.1 - - [23/04/2020 09:44:44] "GET /socket.io/ 404 HTTP/1.1" -127.0.0.1 - - [23/04/2020 09:44:46] "GET /socket.io/ 404 HTTP/1.1" -127.0.0.1 - - [23/04/2020 09:44:57] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:44:59] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:44:59] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:45:00] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:45:00] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:45:02] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 09:47:22] "GET / 200 HTTP/1.0" -127.0.0.1 - - [23/04/2020 10:17:33] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:00:16] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:00:17] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:03:27] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:03:27] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:30] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:30] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:31] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:34] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:35] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:36] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:52] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:22:56] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:23:00] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:24:00] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:24:13] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:24:13] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:24:15] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:24:17] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:32:25] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:32:26] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:32:34] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:32:35] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:32:49] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:32:51] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:33:04] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:33:06] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:33:07] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:33:10] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:33:10] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:34:51] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:35:02] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:35:03] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:35:19] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:35:29] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:35:31] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:37:10] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:37:56] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:38:10] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:38:11] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:38:12] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:38:14] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:39:14] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:39:15] "GET / 500 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:39:30] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:39:32] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:40:41] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:40:43] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:41:00] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:41:12] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:41:50] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:41:51] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:41:52] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:41:58] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:41:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:42:06] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:42:53] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 10:42:54] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:20:46] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:20:46] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:20:47] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:24:22] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:24:22] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:24:23] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:24:23] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:24:24] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:34] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:34] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:35] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:35] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:36] "GET / 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:37] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:39] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:39] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:39] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:40] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:40] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:28:59] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:29:02] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:56:53] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:57:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:57:01] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:57:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:57:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:57:50] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:57:52] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:58:13] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:58:13] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:58:14] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 11:58:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 12:02:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [24/04/2020 12:03:55] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:05:57] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:05:57] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:05:58] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:05:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:06:27] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:06:28] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:06:58] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:06:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:07:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:07:20] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:07:20] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:07:21] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:07:22] "GET /test 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:07:24] "GET /test 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:09:36] "GET /test 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:20:36] "GET /test/escaping 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:26:52] "GET /test/escaping 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:29:14] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:29:32] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:57:48] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:58:07] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 09:58:34] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:01:46] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:02:15] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:02:34] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:02:55] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:03:01] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:04:22] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:06:23] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:06:24] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:06:50] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:06:51] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:06:59] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:07:15] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:07:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:07:18] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:07:18] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:07:19] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:07:19] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:07:29] "GET /login/q 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:18:17] "GET /login/q 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:18:18] "GET /login/q 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:18:30] "GET /login/q 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:20:46] "GET /login/q 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:21:20] "GET /login/q 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:21:22] "GET /login/ 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:21:25] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:21:49] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:21:56] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:22:08] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:03] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:03] "GET /favicon.ico 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:04] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:06] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:06] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:16] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:19] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:26] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:26] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:28] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:31] "GET / 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:35] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:36] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:38] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:38] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:39] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:39] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:23:50] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:50] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:54] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:54] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:56] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:57] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:57] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:58] "GET /home 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:58] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:26:59] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:27:03] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:27:04] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:27:38] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:27:53] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:28:59] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:29:16] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:29:46] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:30:09] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:30:13] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:30:20] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:30:40] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:31:00] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:31:09] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:31:21] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:31:29] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:31:30] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:32:11] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:32:11] "GET /favicon.ico 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:32:43] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:32:43] "GET /favicon.ico 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:32:52] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:32:59] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:08] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:20] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:20] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:26] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:26] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:29] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:36] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:38] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:38] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:39] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:33:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:34:57] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:34:58] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:35:02] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:46:25] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:46:25] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:46:27] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:46:27] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:46:28] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:46:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:49:37] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:49:37] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:49:41] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:49:41] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:49:41] "GET /logout 307 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:49:41] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:15] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:20] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:38] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:38] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:42] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:42] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:47] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:55:52] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:57:59] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:58:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:58:37] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:59:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:59:17] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 10:59:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:00:00] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:03:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:03:10] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:03:15] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:03:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:04:11] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:04:21] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:04:30] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:04:35] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:04:43] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:04:51] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:05:10] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:05:13] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:05:24] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:05:36] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:04] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:04] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:40] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:48] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:48] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:49] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:49] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:06:54] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:07:19] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:07:22] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:07:25] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:11:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:14:46] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:14:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:02] "GET /login 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:11] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:17] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:18] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:18] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:53] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:58] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:15:59] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:16:05] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:16:09] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:16:11] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:16:20] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:16:22] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:16:40] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:17:04] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:17:17] "GET /sqlalchemy 500 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:17:32] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:17:35] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:18:09] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:18:13] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:19:15] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:22] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:22] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:23] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:23] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:23] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:23] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:23] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:23] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:24] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:26] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:29] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:30] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:30] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:30] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:30] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:30] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:34] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:34] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:34] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:34] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:36] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:36] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:20:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:01] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:01] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:03] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:04] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:06] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:20] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:20] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:21] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:22] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:24] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:39] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:47] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:48] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:49] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:49] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:56] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:22:58] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:23:32] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:14] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:14] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:15] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:15] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:16] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:20] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:26] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:24:27] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:11] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:40] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:47] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:52] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:52] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:54] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:55] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:55] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:25:55] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:03] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:23] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:24] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:32] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:34] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:37] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:45] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:48] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:51] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:51] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:51] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:26:58] "GET /sqlalchemy 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:29:53] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:29:54] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:51:16] "GET / 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:51:18] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:51:22] "GET /login/q 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 11:51:23] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:01:56] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:02:35] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:04:29] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:04:36] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:03] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:04] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:04] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:23] "GET /home 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:23] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:31] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:31] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:51] "GET /home 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:51] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:52] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:14:53] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:07] "GET /home 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:07] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:09] "POST /login 303 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:09] "GET /home 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:10] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:17] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:17:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:18:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:19:04] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:19:54] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:20:00] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:20:08] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:20:30] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:20:45] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:20:46] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:21:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:21:07] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:21:44] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:22:04] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:23:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:23:27] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:23:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:23:29] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:23:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:09] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:11] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:19] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:20] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:22] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:32] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:33] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:24:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:25:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:25:26] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:25:27] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:25:35] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:25:36] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:07] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:15] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:22] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:28] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:44] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:26:45] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:11] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:12] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:17] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:18] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:24] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:25] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:25] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:27] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:38] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:39] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:27:53] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:29:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:30:11] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:31:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:31:24] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:38:24] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:38:35] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:38:36] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:38:37] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:39:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:39:51] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:40:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:41:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:41:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:42:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:42:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:42:54] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:42:55] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:43:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:43:02] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:43:15] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:43:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:43:36] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:44:12] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:44:14] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:47:05] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:47:05] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:47:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:56:51] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:56:52] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:57:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:57:40] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:57:42] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:57:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:57:50] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:58:19] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:58:32] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:58:36] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:59:01] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:59:21] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:59:21] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:59:22] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:59:31] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 12:59:45] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:00:16] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:14] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:14] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:29] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:39] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:40] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:41] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:48] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:01:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:02:05] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:02:06] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:06:47] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:06:48] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:06:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:07:11] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:07:40] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:20:51] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:20:51] "GET /favicon.ico 404 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:22:20] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:24:25] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:25:34] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:31:49] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 13:58:00] "GET /login 200 HTTP/1.0" -127.0.0.1 - - [28/04/2020 16:21:51] "GET /200 404 HTTP/1.0" diff --git a/uweb3/scaffold/uweb3_uncaught_exceptions.log b/uweb3/scaffold/uweb3_uncaught_exceptions.log deleted file mode 100644 index a3a81ae1..00000000 --- a/uweb3/scaffold/uweb3_uncaught_exceptions.log +++ /dev/null @@ -1,3296 +0,0 @@ -ERROR: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 141, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 15, in Login - raise Exception("TEST") -Exception: TEST -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 14, in Index - print(id(self.sio)) -AttributeError: 'PageMaker' object has no attribute 'sio' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 19, in Index - q() -NameError: name 'q' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 19, in Index - q() -NameError: name 'q' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 19, in Index - q() -NameError: name 'q' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 15, in magic - return f() -TypeError: Index() missing 1 required positional argument: 'self' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 15, in magic - return f() -TypeError: Index() missing 1 required positional argument: 'self' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 143, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 15, in magic - return f() -TypeError: Index() missing 1 required positional argument: 'self' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 229, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute 'StringEscaping' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 228, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute 'StringEscaping' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 228, in get_response - method, args, hostargs = self.router(path, method, host) - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 116, in request_router - for pattern, handler, routemethod, hostpattern in req_routes: -ValueError: too many values to unpack (expected 4) -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -TypeError: Index() takes 1 positional argument but 2 were given -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute '/' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute '/' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 239, in get_response - return getattr(page_maker, method)(*args) -TypeError: Index() takes 1 positional argument but 2 were given -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 239, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute '/' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 239, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute '/' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -TypeError: Index() takes 1 positional argument but 2 were given -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute '/' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -AttributeError: 'PageMaker' object has no attribute '/' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - method, args, hostargs = self.router(path, method, host) -ValueError: too many values to unpack (expected 3) -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 237, in get_response - method, args, hostargs, test = self.router(path, method, host) -NameError: name 'path' is not defined -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -TypeError: Index() takes 1 positional argument but 2 were given -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -TypeError: Index() takes 1 positional argument but 2 were given -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 238, in get_response - return getattr(page_maker, method)(*args) -TypeError: Index() takes 1 positional argument but 2 were given -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 36, in FourOhFour - return self.parser.Parse('404.html', path=path) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 36, in FourOhFour - return self.parser.Parse('404.html', path=path) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 36, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 27, in Index - return self.parser.Parse('index.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 27, in Index - return self.parser.Parse('index.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 27, in Index - return self.parser.Parse('index.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 27, in Index - return self.parser.Parse('index.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 27, in Index - return self.parser.Parse('index.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 27, in Index - return self.parser.Parse('index.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 27, in Index - return self.parser.Parse('index.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/index.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 31, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 36, in FourOhFour - return self.parser.Parse('404.html', path=path) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 36, in FourOhFour - return self.parser.Parse('404.html', path=path) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 36, in FourOhFour - return self.parser.Parse('404.html', path=path) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/base/pages.py", line 36, in FourOhFour - return self.parser.Parse('404.html', path=path) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/404.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 474, in __init__ - self._file_mtime = os.path.getmtime(self._file_name) - File "/usr/lib/python3.6/genericpath.py", line 55, in getmtime - return os.stat(filename).st_mtime -FileNotFoundError: [Errno 2] No such file or directory: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 185, in AddTemplate - self[name or location] = FileTemplate(template_path, parser=self) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 478, in __init__ - raise TemplateReadError('Cannot open: %r' % template_path) -uweb3.templateparser.TemplateReadError: Cannot open: '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' - -During handling of the above exception, another exception occurred: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 35, in Login - return self.parser.Parse('login.html') - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 204, in Parse - return self[template].Parse(**replacements) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 163, in __getitem__ - self.AddTemplate(template) - File "/home/stef/devel/uweb3/uweb3/templateparser.py", line 187, in AddTemplate - raise TemplateReadError('Could not load template %r' % template_path) -uweb3.templateparser.TemplateReadError: Could not load template '/home/stef/devel/uweb3/uweb3/scaffold/routes/templates/login.html' -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 17, in Login - raise Exception("test") -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 17, in Login - raise Exception("test") -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 236, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 17, in Login - raise Exception("test") -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - if self.req.method == 'POST': -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - if self.req.method == 'POST': -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - if self.req.method == 'POST': -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - if self.req.method == 'POST': -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - if self.req.method == 'POST': -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 19, in Login - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/login.py", line 17, in Login - raise Exception('test') -Exception: test -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1248, in _execute_context - cursor, statement, parameters, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 588, in do_execute - cursor.execute(statement, parameters) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 209, in execute - res = self._query(query) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 315, in _query - db.query(q) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/connections.py", line 239, in query - _mysql.connection.query(self, query) -MySQLdb._exceptions.IntegrityError: (1062, "Duplicate entry 'hello' for key 'username'") - -The above exception was the direct cause of the following exception: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 83, in Sqlalchemy - print(User.Create(self.session, {'username': 'hello', 'password': 'test', 'authorid': 1})) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1543, in Create - return cls(session, record) - File "", line 4, in __init__ - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/state.py", line 433, in _initialize_instance - manager.dispatch.init_failure(self, args, kwargs) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 69, in __exit__ - exc_value, with_traceback=exc_tb, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/state.py", line 430, in _initialize_instance - return manager.original_init(*mixed[1:], **kwargs) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 26, in __init__ - super(User, self).__init__(*args, **kwargs) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1302, in __init__ - self._BuildClassFromRecord(record) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1317, in _BuildClassFromRecord - self.session.commit() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 1036, in commit - self.transaction.commit() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 503, in commit - self._prepare_impl() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 482, in _prepare_impl - self.session.flush() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2496, in flush - self._flush(objects) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2637, in _flush - transaction.rollback(_capture_exception=True) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 69, in __exit__ - exc_value, with_traceback=exc_tb, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2597, in _flush - flush_context.execute() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", line 422, in execute - rec.execute(self) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", line 589, in execute - uow, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", line 245, in save_obj - insert, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", line 1136, in _emit_insert_statements - statement, params - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 984, in execute - return meth(self, multiparams, params) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/sql/elements.py", line 293, in _execute_on_connection - return connection._execute_clauseelement(self, multiparams, params) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1103, in _execute_clauseelement - distilled_params, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1288, in _execute_context - e, statement, parameters, cursor, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1482, in _handle_dbapi_exception - sqlalchemy_exception, with_traceback=exc_info[2], from_=e - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1248, in _execute_context - cursor, statement, parameters, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 588, in do_execute - cursor.execute(statement, parameters) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 209, in execute - res = self._query(query) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 315, in _query - db.query(q) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/connections.py", line 239, in query - _mysql.connection.query(self, query) -sqlalchemy.exc.IntegrityError: (MySQLdb._exceptions.IntegrityError) (1062, "Duplicate entry 'hello' for key 'username'") -[SQL: INSERT INTO alchemy_users (username, password, authorid) VALUES (%s, %s, %s)] -[parameters: ('hello', 'test', 1)] -(Background on this error at: http://sqlalche.me/e/gkpj) -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1248, in _execute_context - cursor, statement, parameters, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 588, in do_execute - cursor.execute(statement, parameters) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 209, in execute - res = self._query(query) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 315, in _query - db.query(q) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/connections.py", line 239, in query - _mysql.connection.query(self, query) -MySQLdb._exceptions.IntegrityError: (1062, "Duplicate entry 'hello' for key 'username'") - -The above exception was the direct cause of the following exception: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 83, in Sqlalchemy - print(User.Create(self.session, {'username': 'hello', 'password': 'test', 'authorid': 1})) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1543, in Create - return cls(session, record) - File "", line 4, in __init__ - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/state.py", line 433, in _initialize_instance - manager.dispatch.init_failure(self, args, kwargs) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 69, in __exit__ - exc_value, with_traceback=exc_tb, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/state.py", line 430, in _initialize_instance - return manager.original_init(*mixed[1:], **kwargs) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 26, in __init__ - super(User, self).__init__(*args, **kwargs) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1302, in __init__ - self._BuildClassFromRecord(record) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1317, in _BuildClassFromRecord - self.session.commit() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 1036, in commit - self.transaction.commit() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 503, in commit - self._prepare_impl() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 482, in _prepare_impl - self.session.flush() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2496, in flush - self._flush(objects) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2637, in _flush - transaction.rollback(_capture_exception=True) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 69, in __exit__ - exc_value, with_traceback=exc_tb, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2597, in _flush - flush_context.execute() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", line 422, in execute - rec.execute(self) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", line 589, in execute - uow, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", line 245, in save_obj - insert, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", line 1136, in _emit_insert_statements - statement, params - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 984, in execute - return meth(self, multiparams, params) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/sql/elements.py", line 293, in _execute_on_connection - return connection._execute_clauseelement(self, multiparams, params) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1103, in _execute_clauseelement - distilled_params, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1288, in _execute_context - e, statement, parameters, cursor, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1482, in _handle_dbapi_exception - sqlalchemy_exception, with_traceback=exc_info[2], from_=e - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1248, in _execute_context - cursor, statement, parameters, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 588, in do_execute - cursor.execute(statement, parameters) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 209, in execute - res = self._query(query) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 315, in _query - db.query(q) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/connections.py", line 239, in query - _mysql.connection.query(self, query) -sqlalchemy.exc.IntegrityError: (MySQLdb._exceptions.IntegrityError) (1062, "Duplicate entry 'hello' for key 'username'") -[SQL: INSERT INTO alchemy_users (username, password, authorid) VALUES (%s, %s, %s)] -[parameters: ('hello', 'test', 1)] -(Background on this error at: http://sqlalche.me/e/gkpj) -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1248, in _execute_context - cursor, statement, parameters, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 588, in do_execute - cursor.execute(statement, parameters) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 209, in execute - res = self._query(query) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 315, in _query - db.query(q) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/connections.py", line 239, in query - _mysql.connection.query(self, query) -MySQLdb._exceptions.IntegrityError: (1062, "Duplicate entry 'hello' for key 'username'") - -The above exception was the direct cause of the following exception: - -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 83, in Sqlalchemy - print(User.Create(self.session, {'username': 'hello', 'password': 'test', 'authorid': 1})) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1543, in Create - return cls(session, record) - File "", line 4, in __init__ - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/state.py", line 433, in _initialize_instance - manager.dispatch.init_failure(self, args, kwargs) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 69, in __exit__ - exc_value, with_traceback=exc_tb, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/state.py", line 430, in _initialize_instance - return manager.original_init(*mixed[1:], **kwargs) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 26, in __init__ - super(User, self).__init__(*args, **kwargs) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1302, in __init__ - self._BuildClassFromRecord(record) - File "/home/stef/devel/uweb3/uweb3/model.py", line 1317, in _BuildClassFromRecord - self.session.commit() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 1036, in commit - self.transaction.commit() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 503, in commit - self._prepare_impl() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 482, in _prepare_impl - self.session.flush() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2496, in flush - self._flush(objects) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2637, in _flush - transaction.rollback(_capture_exception=True) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/langhelpers.py", line 69, in __exit__ - exc_value, with_traceback=exc_tb, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/session.py", line 2597, in _flush - flush_context.execute() - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", line 422, in execute - rec.execute(self) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/unitofwork.py", line 589, in execute - uow, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", line 245, in save_obj - insert, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/orm/persistence.py", line 1136, in _emit_insert_statements - statement, params - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 984, in execute - return meth(self, multiparams, params) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/sql/elements.py", line 293, in _execute_on_connection - return connection._execute_clauseelement(self, multiparams, params) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1103, in _execute_clauseelement - distilled_params, - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1288, in _execute_context - e, statement, parameters, cursor, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1482, in _handle_dbapi_exception - sqlalchemy_exception, with_traceback=exc_info[2], from_=e - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/util/compat.py", line 178, in raise_ - raise exception - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/base.py", line 1248, in _execute_context - cursor, statement, parameters, context - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/sqlalchemy/engine/default.py", line 588, in do_execute - cursor.execute(statement, parameters) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 209, in execute - res = self._query(query) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/cursors.py", line 315, in _query - db.query(q) - File "/home/stef/devel/uweb3/uweb3-venv/lib/python3.6/site-packages/MySQLdb/connections.py", line 239, in query - _mysql.connection.query(self, query) -sqlalchemy.exc.IntegrityError: (MySQLdb._exceptions.IntegrityError) (1062, "Duplicate entry 'hello' for key 'username'") -[SQL: INSERT INTO alchemy_users (username, password, authorid) VALUES (%s, %s, %s)] -[parameters: ('hello', 'test', 1)] -(Background on this error at: http://sqlalche.me/e/gkpj) -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 78, in Sqlalchemy - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 78, in Sqlalchemy - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 78, in Sqlalchemy - raise Exception() -Exception -UNCAUGHT EXCEPTION: -Traceback (most recent call last): - File "/home/stef/devel/uweb3/uweb3/__init__.py", line 235, in get_response - return getattr(page_maker, method)(*args) - File "/home/stef/devel/uweb3/uweb3/scaffold/routes/sqlalchemy.py", line 78, in Sqlalchemy - raise Exception() -Exception From a7369f1c4522dd491cdffcdd6da0a14bf8be9f58 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 13 May 2020 11:43:54 +0200 Subject: [PATCH 009/118] Cleaning up code. Setup.py no longer installs OpenID stuff --- setup.py | 1 - uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py | 2 +- uweb3/pagemaker/__init__.py | 18 ++---------------- 3 files changed, 3 insertions(+), 18 deletions(-) diff --git a/setup.py b/setup.py index bb82a251..35b87f64 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,6 @@ 'decorator', 'PyMySQL', 'python-magic', - 'python3-openid', 'pytz', 'simplejson', 'sqlalchemy', diff --git a/uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py b/uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py index 45facb00..0a8f4110 100644 --- a/uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py +++ b/uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py @@ -5,7 +5,7 @@ # Custom modules -from underdark.libs.sqltalk import sqlresult +from libs.sqltalk import sqlresult class Cursor(object): diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index d35f4b41..e46a3db5 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -192,20 +192,6 @@ def XSRFInvalidToken(self, command): **self.CommonBlocks('Invalid XSRF token')) return uweb3.Response(content=page_data, httpcode=403) - # def _GetLoggedInUser(self): - # """Checks if user is logged in based on cookie""" - # scookie = SecureCookie(self.secure_cookie_connection) - # if not scookie.cookiejar.get('login'): - # return None - # try: - # user = scookie.cookiejar.get('login') - # except Exception: - # self.req.DeleteCookie('login') - # return None - # if not user: - # return None - # return Users(None, user) - @classmethod def LoadModules(cls, default_routes='routes', excluded_files=('__init__', '.pyc')): """Loops over all .py files apart from some exceptions in target directory @@ -440,7 +426,7 @@ def connection(self): """Returns a MySQL database connection.""" try: if '__mysql' not in self.persistent: - from underdark.libs.sqltalk import mysql + from libs.sqltalk import mysql mysql_config = self.options['mysql'] self.persistent.Set('__mysql', mysql.Connect( host=mysql_config.get('host', 'localhost'), @@ -462,7 +448,7 @@ class SqliteMixin(object): def connection(self): """Returns an SQLite database connection.""" if '__sqlite' not in self.persistent: - from underdark.libs.sqltalk import sqlite + from libs.sqltalk import sqlite self.persistent.Set('__sqlite', sqlite.Connect( self.options['sqlite']['database'])) return self.persistent.Get('__sqlite') From b6bd0ffe1643d6deee123092626b7fad042c0d5f Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 13 May 2020 12:06:01 +0200 Subject: [PATCH 010/118] Fixed pathing. Added a feature that should rollback the database if an internal server error occurs --- uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py | 2 +- uweb3/pagemaker/__init__.py | 17 ++++++++++++----- uweb3/request.py | 2 +- uweb3/test_request.py | 2 +- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py b/uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py index 0a8f4110..f6410a7d 100644 --- a/uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py +++ b/uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py @@ -5,7 +5,7 @@ # Custom modules -from libs.sqltalk import sqlresult +from ext_libs.libs.sqltalk import sqlresult class Cursor(object): diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index e46a3db5..6674d6b5 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -174,16 +174,22 @@ def __init__(self, req, config=None, secure_cookie_secret=None, executing_path=N self.options = config or {} self.persistent = self.PERSISTENT self.secure_cookie_connection = (self.req, self.cookies, secure_cookie_secret) - # self.user = self._GetLoggedInUser() def _PostRequest(self, response): if response.status == '500 Internal Server Error': if not hasattr(self, 'connection_error'): #this is set when we try and create a connection but it failed + #TODO: This requires some testing + print("ATTEMPTING TO ROLLBACK DATABASE") + try: + with self.connection as cursor: + cursor.Execute("ROLLBACK") + except Exception: + if hasattr(self, 'connection'): + if self.connection.open: + self.connection.close() + self.persistent.Del("__mysql") self.connection_error = False - if hasattr(self, 'connection'): - if self.connection.open: - self.connection.close() - self.persistent.Del("__mysql") + return response def XSRFInvalidToken(self, command): @@ -282,6 +288,7 @@ def CommonBlocks(self, title, page_id=None, scripts=None): if not page_id: page_id = title.replace(' ', '_').lower() + #TODO: self.user is no more return {'header': self.parser.Parse( 'header.html', title=title, page_id=page_id, user=self.user ), diff --git a/uweb3/request.py b/uweb3/request.py index 7fd57e40..5fa94cfe 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -17,7 +17,7 @@ import re import json # uWeb modules -from . import response +from uweb3 import response from werkzeug.formparser import parse_form_data from werkzeug.datastructures import MultiDict diff --git a/uweb3/test_request.py b/uweb3/test_request.py index 1967cfef..e4d5c7c5 100644 --- a/uweb3/test_request.py +++ b/uweb3/test_request.py @@ -17,7 +17,7 @@ import urllib # Unittest target -from . import request +import request class IndexedFieldStorageTest(unittest.TestCase): From 5c337d31d04f633f89975ed5eaac5f58e14580f1 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 13 May 2020 12:38:35 +0200 Subject: [PATCH 011/118] Added a comment to the source code of the StaticMiddleware helper class that we use from WSGI --- uweb3/__init__.py | 2 -- uweb3/helpers.py | 3 ++- uweb3/scaffold/base/static/uweb3_template.zip | Bin 95660 -> 0 bytes 3 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 uweb3/scaffold/base/static/uweb3_template.zip diff --git a/uweb3/__init__.py b/uweb3/__init__.py index a1b04dc1..662bae5d 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -18,7 +18,6 @@ import socket, errno import datetime - # Add the ext_lib directory to the path sys.path.append( os.path.abspath(os.path.join(os.path.dirname(__file__), 'ext_lib'))) @@ -175,7 +174,6 @@ def __init__(self, page_class, routes, executing_path=None): self.secure_cookie_secret = str(os.urandom(32)) self.setup_routing() - def __call__(self, env, start_response): """WSGI request handler. Accepts the WSGI `environment` dictionary and a function to start the diff --git a/uweb3/helpers.py b/uweb3/helpers.py index 09fd81b3..c33fb618 100644 --- a/uweb3/helpers.py +++ b/uweb3/helpers.py @@ -102,7 +102,8 @@ def http404(env, start_response): [('Content-type', 'text/plain; charset=utf-8')]) return [b'404 Not Found'] - +#This code is copied and altered from the WSGI static middleware PyPi package +#https://pypi.org/project/wsgi-static-middleware/ class StaticMiddleware: CACHE_DURATION = MimeTypeDict({'text': 7, 'image': 30, 'application': 7}) diff --git a/uweb3/scaffold/base/static/uweb3_template.zip b/uweb3/scaffold/base/static/uweb3_template.zip deleted file mode 100644 index 9b8c94c51df44d189a27bb37ea239967f86f99ae..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 95660 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

}lM( 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 From 15287990cc7761b0799369bcdd09710ba5c99ca8 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 13 May 2020 13:18:30 +0200 Subject: [PATCH 012/118] Removed new_login file. uWeb3 no longer needs support for a loginmixin --- README.md | 5 - uweb3/pagemaker/__init__.py | 1 - uweb3/pagemaker/new_login.py | 148 ---------------------------- uweb3/scaffold/nohup.out | 0 uweb3/scaffold/routes/sqlalchemy.py | 148 +++++++++++++++++++++++++++- uweb3/scaffold/routes/test.py | 2 - 6 files changed, 146 insertions(+), 158 deletions(-) delete mode 100644 uweb3/pagemaker/new_login.py delete mode 100644 uweb3/scaffold/nohup.out diff --git a/README.md b/README.md index 2c9bfe87..6f9e1511 100644 --- a/README.md +++ b/README.md @@ -96,11 +96,6 @@ After creating your pagemaker be sure to add the route endpoint to routes list i - Method called DeleteCookie - A if statement that checks string like cookies and raises an error if the size is equal or bigger than 4096 bytes. - AddCookie method, edited this and the response class to handle the setting of multiple cookies. Previously setting multiple cookies with the Set-Cookie header would make the last cookie the only cookie. -- In pagemaker/new_login Users class: - - Create user - - Find user by name - - Create a cookie with userID + secret - - Validate if user messed with given cookie and render it useless if so - In pagemaker/new_decorators: - Loggedin decorator that validates if user is loggedin based on cookie with userid - Checkxsrf decorator that checks if the incorrect_xsrf_token flag is set diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 6674d6b5..95091524 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -16,7 +16,6 @@ from uweb3.model import SecureCookie from .. import response, templateparser -from .new_login import Users RFC_1123_DATE = '%a, %d %b %Y %T GMT' diff --git a/uweb3/pagemaker/new_login.py b/uweb3/pagemaker/new_login.py deleted file mode 100644 index 8ac543f7..00000000 --- a/uweb3/pagemaker/new_login.py +++ /dev/null @@ -1,148 +0,0 @@ -import hashlib - -import bcrypt - -from .. import model - -class UserCookieInvalidError(Exception): - """Superclass for errors returned by the user class.""" - -class Test(model.SettingsManager): - """ """ - -class UserCookie(model.SecureCookie): - """ """ - -class Users(model.Record): - """ """ - salt = "SomeSaltyBoi" - cookie_salt = "SomeSaltyCookie" - - UserCookieInvalidError = UserCookieInvalidError - - @classmethod - def CreateNew(cls, connection, user): - """Creates new user if not existing - - Arguments: - @ connection: sqltalk database connection object - @ user: dict. username and password keys are required. - Returns: - ValueError: if username/password are not set - AlreadyExistsError: if username already in database - Users: Users object when user is created - """ - if not user.get('username'): - raise ValueError('Username required') - if not user.get('password'): - raise ValueError('Password required') - - try: - cls.FromName(connection, user.get('username')) - return cls.AlreadyExistError("User with name '{}' already exists".format(user.get('username'))) - except cls.NotExistError: - user['password'] = cls.__HashPassword(user.get('password')).decode('utf-8') - return cls.Create(connection, user) - - @classmethod - def FromName(cls, connection, username): - """Select a user from the database based on name - Arguments: - @ username: str - Returns: - NotExistError: raised when no user with given username found - Users: Users object with the connection and all relevant user data - """ - from sqlalchemy import Table, MetaData, Column, Integer, String, text - meta = MetaData() - users_table = Table('users', meta, - Column('id', Integer, primary_key=True), - Column('username', String(255)), - Column('password', String(255)), - ) - # result = connection.execute(users_table.select()) - # statement = text("SELECT * FROM users WHERE username = :name") - # user = connection.execute(statement, {'name': username}).fetchone() - with connection as cursor: - safe_name = connection.EscapeValues(username) - user = cursor.Select( - table='users', - conditions='username={}'.format(safe_name)) - if not user: - raise cls.NotExistError('No user with name {}'.format(username)) - return cls(connection, user[0]) - - - @classmethod - def __HashPassword(cls, password): - """Hash password with bcrypt""" - password = password + cls.salt - - return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) - - @classmethod - def ComparePassword(cls, password, hashed): - """Check if passwords match - - Arguments: - @ password: str - @ hashed: str password hash from users database table - Returns: - Boolean: True if match False if not - """ - if not isinstance(hashed, bytes): - hashed = hashed.encode('utf-8') - password = password + cls.salt - return bcrypt.checkpw(password.encode('utf-8'), hashed) - - @classmethod - def CreateValidationCookieHash(cls, data): - """Takes a non nested dictionary and turns it into a secure cookie. - - Required: - @ id: str/int - Returns: - A string that is ready to be placed in a cookie. Hash and data are seperated by a + - """ - if not data.get('id'): - raise ValueError("id is required") - - cookie_dict = {} - string_to_hash = "" - for key in data.keys(): - if not isinstance(data[key], (str, int)): - raise ValueError('{} must be of type str or int'.format(data[key])) - value = str(data[key]) - string_to_hash += value - cookie_dict[key] = value - - hashed = (string_to_hash + cls.cookie_salt).encode('utf-8') - h = hashlib.new('ripemd160') - h.update(hashed) - return '{}+{}'.format(h.hexdigest(), cookie_dict) - - @classmethod - def ValidateUserCookie(cls, cookie): - """Takes a cookie and validates it - Arguments - @ str: A hashed cookie from the `CreateValidationCookieHash` method - """ - from ast import literal_eval - if not cookie: - return None - - try: - data = cookie.rsplit('+', 1)[1] - data = literal_eval(data) - except Exception: - raise cls.UserCookieInvalidError("Invalid cookie") - - user_id = data.get('id', None) - if not user_id: - raise cls.UserCookieInvalidError("Could not get id from cookie") - - if cookie != cls.CreateValidationCookieHash(data): - raise cls.UserCookieInvalidError("Invalid cookie") - - return user_id - \ No newline at end of file diff --git a/uweb3/scaffold/nohup.out b/uweb3/scaffold/nohup.out deleted file mode 100644 index e69de29b..00000000 diff --git a/uweb3/scaffold/routes/sqlalchemy.py b/uweb3/scaffold/routes/sqlalchemy.py index e41c405d..cfe54767 100644 --- a/uweb3/scaffold/routes/sqlalchemy.py +++ b/uweb3/scaffold/routes/sqlalchemy.py @@ -3,15 +3,159 @@ from uweb3 import SqAlchemyPageMaker from uweb3.alchemy_model import AlchemyRecord -from uweb3.pagemaker.new_login import Users, UserCookie, Test from uweb3.pagemaker.new_decorators import checkxsrf from sqlalchemy import Column, Integer, String, update, MetaData, Table, ForeignKey, inspect from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship, lazyload - +from uweb3 import model Base = declarative_base() +import hashlib +import bcrypt + +class UserCookieInvalidError(Exception): + """Superclass for errors returned by the user class.""" + +class Test(model.SettingsManager): + """ """ + +class UserCookie(model.SecureCookie): + """ """ + +class Users(model.Record): + """ """ + salt = "SomeSaltyBoi" + cookie_salt = "SomeSaltyCookie" + + UserCookieInvalidError = UserCookieInvalidError + + @classmethod + def CreateNew(cls, connection, user): + """Creates new user if not existing + + Arguments: + @ connection: sqltalk database connection object + @ user: dict. username and password keys are required. + Returns: + ValueError: if username/password are not set + AlreadyExistsError: if username already in database + Users: Users object when user is created + """ + if not user.get('username'): + raise ValueError('Username required') + if not user.get('password'): + raise ValueError('Password required') + + try: + cls.FromName(connection, user.get('username')) + return cls.AlreadyExistError("User with name '{}' already exists".format(user.get('username'))) + except cls.NotExistError: + user['password'] = cls.__HashPassword(user.get('password')).decode('utf-8') + return cls.Create(connection, user) + + @classmethod + def FromName(cls, connection, username): + """Select a user from the database based on name + Arguments: + @ username: str + Returns: + NotExistError: raised when no user with given username found + Users: Users object with the connection and all relevant user data + """ + from sqlalchemy import Table, MetaData, Column, Integer, String, text + meta = MetaData() + users_table = Table('users', meta, + Column('id', Integer, primary_key=True), + Column('username', String(255)), + Column('password', String(255)), + ) + # result = connection.execute(users_table.select()) + # statement = text("SELECT * FROM users WHERE username = :name") + # user = connection.execute(statement, {'name': username}).fetchone() + with connection as cursor: + safe_name = connection.EscapeValues(username) + user = cursor.Select( + table='users', + conditions='username={}'.format(safe_name)) + if not user: + raise cls.NotExistError('No user with name {}'.format(username)) + return cls(connection, user[0]) + + + @classmethod + def __HashPassword(cls, password): + """Hash password with bcrypt""" + password = password + cls.salt + + return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) + + @classmethod + def ComparePassword(cls, password, hashed): + """Check if passwords match + + Arguments: + @ password: str + @ hashed: str password hash from users database table + Returns: + Boolean: True if match False if not + """ + if not isinstance(hashed, bytes): + hashed = hashed.encode('utf-8') + password = password + cls.salt + return bcrypt.checkpw(password.encode('utf-8'), hashed) + + @classmethod + def CreateValidationCookieHash(cls, data): + """Takes a non nested dictionary and turns it into a secure cookie. + + Required: + @ id: str/int + Returns: + A string that is ready to be placed in a cookie. Hash and data are seperated by a + + """ + if not data.get('id'): + raise ValueError("id is required") + + cookie_dict = {} + string_to_hash = "" + for key in data.keys(): + if not isinstance(data[key], (str, int)): + raise ValueError('{} must be of type str or int'.format(data[key])) + value = str(data[key]) + string_to_hash += value + cookie_dict[key] = value + + hashed = (string_to_hash + cls.cookie_salt).encode('utf-8') + h = hashlib.new('ripemd160') + h.update(hashed) + return '{}+{}'.format(h.hexdigest(), cookie_dict) + + @classmethod + def ValidateUserCookie(cls, cookie): + """Takes a cookie and validates it + Arguments + @ str: A hashed cookie from the `CreateValidationCookieHash` method + """ + from ast import literal_eval + if not cookie: + return None + + try: + data = cookie.rsplit('+', 1)[1] + data = literal_eval(data) + except Exception: + raise cls.UserCookieInvalidError("Invalid cookie") + + user_id = data.get('id', None) + if not user_id: + raise cls.UserCookieInvalidError("Could not get id from cookie") + + if cookie != cls.CreateValidationCookieHash(data): + raise cls.UserCookieInvalidError("Invalid cookie") + + return user_id + class User(AlchemyRecord, Base): __tablename__ = 'alchemy_users' diff --git a/uweb3/scaffold/routes/test.py b/uweb3/scaffold/routes/test.py index 23c49b9c..264b47bb 100644 --- a/uweb3/scaffold/routes/test.py +++ b/uweb3/scaffold/routes/test.py @@ -4,8 +4,6 @@ import uweb3 import json from uweb3 import PageMaker -from uweb3.pagemaker.new_login import UserCookie -from uweb3.pagemaker.new_decorators import loggedin, checkxsrf from uweb3.ext_lib.libs.safestring import SQLSAFE, HTMLsafestring from uweb3.model import SettingsManager From 5c1f9dfd8c1850b00cea9e49383a4ce821e9fb21 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 13 May 2020 13:22:14 +0200 Subject: [PATCH 013/118] removed left over code --- uweb3/scaffold/routes/sqlalchemy.py | 148 +--------------------------- 1 file changed, 1 insertion(+), 147 deletions(-) diff --git a/uweb3/scaffold/routes/sqlalchemy.py b/uweb3/scaffold/routes/sqlalchemy.py index cfe54767..6aac9729 100644 --- a/uweb3/scaffold/routes/sqlalchemy.py +++ b/uweb3/scaffold/routes/sqlalchemy.py @@ -3,158 +3,12 @@ from uweb3 import SqAlchemyPageMaker from uweb3.alchemy_model import AlchemyRecord -from uweb3.pagemaker.new_decorators import checkxsrf from sqlalchemy import Column, Integer, String, update, MetaData, Table, ForeignKey, inspect from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker, relationship, lazyload -from uweb3 import model -Base = declarative_base() -import hashlib -import bcrypt - -class UserCookieInvalidError(Exception): - """Superclass for errors returned by the user class.""" - -class Test(model.SettingsManager): - """ """ - -class UserCookie(model.SecureCookie): - """ """ - -class Users(model.Record): - """ """ - salt = "SomeSaltyBoi" - cookie_salt = "SomeSaltyCookie" - - UserCookieInvalidError = UserCookieInvalidError - - @classmethod - def CreateNew(cls, connection, user): - """Creates new user if not existing - - Arguments: - @ connection: sqltalk database connection object - @ user: dict. username and password keys are required. - Returns: - ValueError: if username/password are not set - AlreadyExistsError: if username already in database - Users: Users object when user is created - """ - if not user.get('username'): - raise ValueError('Username required') - if not user.get('password'): - raise ValueError('Password required') - - try: - cls.FromName(connection, user.get('username')) - return cls.AlreadyExistError("User with name '{}' already exists".format(user.get('username'))) - except cls.NotExistError: - user['password'] = cls.__HashPassword(user.get('password')).decode('utf-8') - return cls.Create(connection, user) - - @classmethod - def FromName(cls, connection, username): - """Select a user from the database based on name - Arguments: - @ username: str - Returns: - NotExistError: raised when no user with given username found - Users: Users object with the connection and all relevant user data - """ - from sqlalchemy import Table, MetaData, Column, Integer, String, text - meta = MetaData() - users_table = Table('users', meta, - Column('id', Integer, primary_key=True), - Column('username', String(255)), - Column('password', String(255)), - ) - # result = connection.execute(users_table.select()) - # statement = text("SELECT * FROM users WHERE username = :name") - # user = connection.execute(statement, {'name': username}).fetchone() - with connection as cursor: - safe_name = connection.EscapeValues(username) - user = cursor.Select( - table='users', - conditions='username={}'.format(safe_name)) - if not user: - raise cls.NotExistError('No user with name {}'.format(username)) - return cls(connection, user[0]) - - - @classmethod - def __HashPassword(cls, password): - """Hash password with bcrypt""" - password = password + cls.salt - - return bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt()) - - @classmethod - def ComparePassword(cls, password, hashed): - """Check if passwords match - - Arguments: - @ password: str - @ hashed: str password hash from users database table - Returns: - Boolean: True if match False if not - """ - if not isinstance(hashed, bytes): - hashed = hashed.encode('utf-8') - password = password + cls.salt - return bcrypt.checkpw(password.encode('utf-8'), hashed) - - @classmethod - def CreateValidationCookieHash(cls, data): - """Takes a non nested dictionary and turns it into a secure cookie. - - Required: - @ id: str/int - Returns: - A string that is ready to be placed in a cookie. Hash and data are seperated by a + - """ - if not data.get('id'): - raise ValueError("id is required") - - cookie_dict = {} - string_to_hash = "" - for key in data.keys(): - if not isinstance(data[key], (str, int)): - raise ValueError('{} must be of type str or int'.format(data[key])) - value = str(data[key]) - string_to_hash += value - cookie_dict[key] = value - - hashed = (string_to_hash + cls.cookie_salt).encode('utf-8') - h = hashlib.new('ripemd160') - h.update(hashed) - return '{}+{}'.format(h.hexdigest(), cookie_dict) - - @classmethod - def ValidateUserCookie(cls, cookie): - """Takes a cookie and validates it - Arguments - @ str: A hashed cookie from the `CreateValidationCookieHash` method - """ - from ast import literal_eval - if not cookie: - return None - - try: - data = cookie.rsplit('+', 1)[1] - data = literal_eval(data) - except Exception: - raise cls.UserCookieInvalidError("Invalid cookie") - - user_id = data.get('id', None) - if not user_id: - raise cls.UserCookieInvalidError("Could not get id from cookie") - - if cookie != cls.CreateValidationCookieHash(data): - raise cls.UserCookieInvalidError("Invalid cookie") - - return user_id +Base = declarative_base() class User(AlchemyRecord, Base): __tablename__ = 'alchemy_users' From b0d1af829d65dda5f349136e949bce3560dd3e29 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 13 May 2020 13:26:45 +0200 Subject: [PATCH 014/118] Remove scaffold from uWeb3 --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 50e96e73..22280549 100644 --- a/.gitignore +++ b/.gitignore @@ -33,4 +33,5 @@ uweb3-venv features *.vscode *.log -*.out \ No newline at end of file +*.out +scaffold/ \ No newline at end of file From 77039031ba30793034d2213c02987591f4a89beb Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 13 May 2020 13:27:16 +0200 Subject: [PATCH 015/118] Remove scaffold from uWeb3 --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 22280549..a416eb13 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ features *.vscode *.log *.out -scaffold/ \ No newline at end of file +*/scaffold/* \ No newline at end of file From 5b99ea74644dc01aa7960e256d64ed0f7b867163 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 13 May 2020 13:30:32 +0200 Subject: [PATCH 016/118] Should now propperly ignore scaffold --- .gitignore | 2 +- uweb3/scaffold/base.wsgi | 16 - uweb3/scaffold/base/README.md | 14 - uweb3/scaffold/base/__init__.py | 35 - uweb3/scaffold/base/config.ini | 18 - uweb3/scaffold/base/pages.py | 18 - uweb3/scaffold/base/static/css/base.css | 1198 ----------------- uweb3/scaffold/base/static/css/layout.css | 1146 ---------------- uweb3/scaffold/base/static/css/module.css | 710 ---------- uweb3/scaffold/base/static/css/theme.css | 206 --- uweb3/scaffold/base/static/scripts/ajax.js | 234 ---- .../base/static/scripts/uweb-dynamic.js | 176 --- .../static/scripts/uweb3-template-parser.js | 175 --- uweb3/scaffold/base/templates/403.html | 1 - uweb3/scaffold/base/templates/404.html | 12 - uweb3/scaffold/base/templates/footer.html | 0 uweb3/scaffold/base/templates/header.html | 0 uweb3/scaffold/base/templates/index.html | 35 - uweb3/scaffold/base/templates/sqlalchemy.html | 39 - uweb3/scaffold/base/templates/test.html | 54 - uweb3/scaffold/base/templates/test2.html | 51 - uweb3/scaffold/routes/__init__.py | 0 uweb3/scaffold/routes/socket_handler.py | 15 - uweb3/scaffold/routes/sqlalchemy.py | 105 -- uweb3/scaffold/routes/test.py | 60 - uweb3/scaffold/serve.py | 31 - 26 files changed, 1 insertion(+), 4350 deletions(-) delete mode 100644 uweb3/scaffold/base.wsgi delete mode 100644 uweb3/scaffold/base/README.md delete mode 100644 uweb3/scaffold/base/__init__.py delete mode 100644 uweb3/scaffold/base/config.ini delete mode 100644 uweb3/scaffold/base/pages.py delete mode 100644 uweb3/scaffold/base/static/css/base.css delete mode 100644 uweb3/scaffold/base/static/css/layout.css delete mode 100644 uweb3/scaffold/base/static/css/module.css delete mode 100644 uweb3/scaffold/base/static/css/theme.css delete mode 100644 uweb3/scaffold/base/static/scripts/ajax.js delete mode 100644 uweb3/scaffold/base/static/scripts/uweb-dynamic.js delete mode 100644 uweb3/scaffold/base/static/scripts/uweb3-template-parser.js delete mode 100644 uweb3/scaffold/base/templates/403.html delete mode 100644 uweb3/scaffold/base/templates/404.html delete mode 100644 uweb3/scaffold/base/templates/footer.html delete mode 100644 uweb3/scaffold/base/templates/header.html delete mode 100644 uweb3/scaffold/base/templates/index.html delete mode 100644 uweb3/scaffold/base/templates/sqlalchemy.html delete mode 100644 uweb3/scaffold/base/templates/test.html delete mode 100644 uweb3/scaffold/base/templates/test2.html delete mode 100644 uweb3/scaffold/routes/__init__.py delete mode 100644 uweb3/scaffold/routes/socket_handler.py delete mode 100644 uweb3/scaffold/routes/sqlalchemy.py delete mode 100644 uweb3/scaffold/routes/test.py delete mode 100644 uweb3/scaffold/serve.py diff --git a/.gitignore b/.gitignore index a416eb13..22280549 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ features *.vscode *.log *.out -*/scaffold/* \ No newline at end of file +scaffold/ \ No newline at end of file diff --git a/uweb3/scaffold/base.wsgi b/uweb3/scaffold/base.wsgi deleted file mode 100644 index 3090755c..00000000 --- a/uweb3/scaffold/base.wsgi +++ /dev/null @@ -1,16 +0,0 @@ -"""WSGI script for Apache mod_wsgi - -For more information about running and configuring mod_wsgi, please refer to -documentation at https://code.google.com/p/modwsgi/wiki/DeveloperGuidelines. -""" - -# Add the current directory to site packages, this allows importing of the -# project. For production, installing this into a virtualenv is recommended. -import os -import site -#site.addsitedir('/path/to/virtualenv/site-packages') -site.addsitedir(os.path.dirname(__file__)) - -# Import the project and create a WSGI application object -import base -application = base.main() diff --git a/uweb3/scaffold/base/README.md b/uweb3/scaffold/base/README.md deleted file mode 100644 index 9b4fa758..00000000 --- a/uweb3/scaffold/base/README.md +++ /dev/null @@ -1,14 +0,0 @@ -# µWeb3 application scaffold - -This is an empty µWeb3 base project that serves as the starting point for your -project. The following parts are included and allow for easy extension: - -* µWeb3 request routing and server setup (in `__init__.py`) -* a basic configuration file that is read upon app start (`config.ini`) -* use of PageMaker (in 'pages.py') and example template usage (templates in `templates/`) -* included Apache WSGI configuration and development server runner. - -# How to run - -* Run `serve.py` from the commandline -* Use the included `base.wsgi` script to set up Apache + mod_wsgi diff --git a/uweb3/scaffold/base/__init__.py b/uweb3/scaffold/base/__init__.py deleted file mode 100644 index 3df73e8b..00000000 --- a/uweb3/scaffold/base/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -"""A minimal uWeb3 project scaffold.""" - -# Standard modules -import os - -# Third-party modules -import uweb3 - -# Application -from . import pages - -def main(sio=None): - """Creates a uWeb3 application. - - The application is created from the following components: - - - The presenter class (PageMaker) which implements the request handlers. - - The routes iterable, where each 2-tuple defines a url-pattern and the - name of a presenter method which should handle it. - - The configuration file (ini format) from which settings should be read. - """ - path = os.path.dirname(os.path.abspath(__file__)) - routes = ( - ('/', 'Index'), - #test routes - ('/sqlalchemy', 'Sqlalchemy'), - ('/test', 'Test'), - ('/getrawtemplate.*', 'GetRawTemplate'), - ('/parsed', 'Parsed'), - ('/test/escaping', 'StringEscaping'), - # (sio.on('test', namespace="/namespace"), 'EventHandler'), - # (sio.on('connect'), 'Connect'), - ('/(.*)', 'FourOhFour')) - - return uweb3.uWeb(pages.PageMaker, routes, executing_path=path) diff --git a/uweb3/scaffold/base/config.ini b/uweb3/scaffold/base/config.ini deleted file mode 100644 index d9ea0622..00000000 --- a/uweb3/scaffold/base/config.ini +++ /dev/null @@ -1,18 +0,0 @@ -[development] -# By default, all logging is enabled, the following two -# configuration options can be used to disable them -access_logging = True -error_logging = True -port = 8000 -dev = True -uweb_dev = True - -[mysql] -host = 127.0.0.1 -user = stef -password = 24192419 -database = uweb - -[routing] -disable_automatic_route_detection = False -default_routing = routes diff --git a/uweb3/scaffold/base/pages.py b/uweb3/scaffold/base/pages.py deleted file mode 100644 index cd42f3b8..00000000 --- a/uweb3/scaffold/base/pages.py +++ /dev/null @@ -1,18 +0,0 @@ -#!/usr/bin/python -"""Request handlers for the uWeb3 project scaffold""" - -from uweb3 import response -from uweb3.model import SettingsManager -from uweb3 import DebuggingPageMaker - -class PageMaker(DebuggingPageMaker): - """Holds all the request handlers for the application""" - - def Index(self): - """Returns the index template""" - return self.parser.Parse('index.html') - - def FourOhFour(self, path): - """The request could not be fulfilled, this returns a 404.""" - self.req.response.httpcode = 404 - return self.parser.Parse('404.html', path=path) diff --git a/uweb3/scaffold/base/static/css/base.css b/uweb3/scaffold/base/static/css/base.css deleted file mode 100644 index 95740706..00000000 --- a/uweb3/scaffold/base/static/css/base.css +++ /dev/null @@ -1,1198 +0,0 @@ -/* This is the Underdark base CSS. Its structure is based on SMACSS. See -https://smacss.com for more info. */ - -/*! normalize.css v4.1.1 | MIT License | github.com/necolas/normalize.css */ - -/** - * 1. Change the default font family in all browsers (opinionated). - * 2. Prevent adjustments of font size after orientation changes in IE and iOS. - */ - -html { - font-family: sans-serif; /* 1 */ - -ms-text-size-adjust: 100%; /* 2 */ - -webkit-text-size-adjust: 100%; /* 2 */ -} - -/** - * Remove the margin in all browsers (opinionated). - */ - -body { - margin: 0; -} - -/* HTML5 display definitions - ========================================================================== */ - -/** - * Add the correct display in IE 9-. - * 1. Add the correct display in Edge, IE, and Firefox. - * 2. Add the correct display in IE. - */ - -article, -aside, -details, /* 1 */ -figcaption, -figure, -footer, -header, -main, /* 2 */ -menu, -nav, -section, -summary { /* 1 */ - display: block; -} - -/** - * Add the correct display in IE 9-. - */ - -audio, -canvas, -progress, -video { - display: inline-block; -} - -/** - * Add the correct display in iOS 4-7. - */ - -audio:not([controls]) { - display: none; - height: 0; -} - -/** - * Add the correct vertical alignment in Chrome, Firefox, and Opera. - */ - -progress { - vertical-align: baseline; -} - -/** - * Add the correct display in IE 10-. - * 1. Add the correct display in IE. - */ - -template, /* 1 */ -[hidden] { - display: none; -} - -/* Links - ========================================================================== */ - -/** - * 1. Remove the gray background on active links in IE 10. - * 2. Remove gaps in links underline in iOS 8+ and Safari 8+. - */ - -a { - background-color: transparent; /* 1 */ - -webkit-text-decoration-skip: objects; /* 2 */ -} - -/** - * Remove the outline on focused links when they are also active or hovered - * in all browsers (opinionated). - */ - -a:active, -a:hover { - outline-width: 0; -} - -/* Text-level semantics - ========================================================================== */ - -/** - * 1. Remove the bottom border in Firefox 39-. - * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. - */ - -abbr[title] { - border-bottom: none; /* 1 */ - text-decoration: underline; /* 2 */ - text-decoration: underline dotted; /* 2 */ -} - -/** - * Prevent the duplicate application of `bolder` by the next rule in Safari 6. - */ - -b, -strong { - font-weight: inherit; -} - -/** - * Add the correct font weight in Chrome, Edge, and Safari. - */ - -b, -strong { - font-weight: bolder; -} - -/** - * Add the correct font style in Android 4.3-. - */ - -dfn { - font-style: italic; -} - -/** - * Correct the font size and margin on `h1` elements within `section` and - * `article` contexts in Chrome, Firefox, and Safari. - */ - -h1 { - font-size: 2em; - margin: 0.67em 0; -} - -/** - * Add the correct background and color in IE 9-. - */ - -mark { - background-color: #ff0; - color: #000; -} - -/** - * Add the correct font size in all browsers. - */ - -small { - font-size: 80%; -} - -/** - * Prevent `sub` and `sup` elements from affecting the line height in - * all browsers. - */ - -sub, -sup { - font-size: 75%; - line-height: 0; - position: relative; - vertical-align: baseline; -} - -sub { - bottom: -0.25em; -} - -sup { - top: -0.5em; -} - -/* Embedded content - ========================================================================== */ - -/** - * Remove the border on images inside links in IE 10-. - */ - -img { - border-style: none; -} - -/** - * Hide the overflow in IE. - */ - -svg:not(:root) { - overflow: hidden; -} - -/* Grouping content - ========================================================================== */ - -/** - * 1. Correct the inheritance and scaling of font size in all browsers. - * 2. Correct the odd `em` font sizing in all browsers. - */ - -code, -kbd, -pre, -samp { - font-family: monospace, monospace; /* 1 */ - font-size: 1em; /* 2 */ -} - -/** - * Add the correct margin in IE 8. - */ - -figure { - margin: 1em 40px; -} - -/** - * 1. Add the correct box sizing in Firefox. - * 2. Show the overflow in Edge and IE. - */ - -hr { - box-sizing: content-box; /* 1 */ - height: 0; /* 1 */ - overflow: visible; /* 2 */ -} - -/* Forms - ========================================================================== */ - -/** - * 1. Change font properties to `inherit` in all browsers (opinionated). - * 2. Remove the margin in Firefox and Safari. - */ - -button, -input, -optgroup, -select, -textarea { - font: inherit; /* 1 */ - margin: 0; /* 2 */ -} - -/** - * Restore the font weight unset by the previous rule. - */ - -optgroup { - font-weight: bold; -} - -/** - * Show the overflow in IE. - * 1. Show the overflow in Edge. - */ - -button, -input { /* 1 */ - overflow: visible; -} - -/** - * Remove the inheritance of text transform in Edge, Firefox, and IE. - * 1. Remove the inheritance of text transform in Firefox. - */ - -button, -select { /* 1 */ - text-transform: none; -} - -/** - * 1. Prevent a WebKit bug where (2) destroys native `audio` and `video` - * controls in Android 4. - * 2. Correct the inability to style clickable types in iOS and Safari. - */ - -button, -html [type="button"], /* 1 */ -[type="reset"], -[type="submit"] { - -webkit-appearance: button; /* 2 */ -} - -/** - * Remove the inner border and padding in Firefox. - */ - -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner { - border-style: none; - padding: 0; -} - -/** - * Restore the focus styles unset by the previous rule. - */ - -button:-moz-focusring, -[type="button"]:-moz-focusring, -[type="reset"]:-moz-focusring, -[type="submit"]:-moz-focusring { - outline: 1px dotted ButtonText; -} - -/** - * Change the border, margin, and padding in all browsers (opinionated). - */ - -fieldset { - border: 1px solid #c0c0c0; - margin: 0 2px; - padding: 0.35em 0.625em 0.75em; -} - -/** - * 1. Correct the text wrapping in Edge and IE. - * 2. Correct the color inheritance from `fieldset` elements in IE. - * 3. Remove the padding so developers are not caught out when they zero out - * `fieldset` elements in all browsers. - */ - -legend { - box-sizing: border-box; /* 1 */ - color: inherit; /* 2 */ - display: table; /* 1 */ - max-width: 100%; /* 1 */ - padding: 0; /* 3 */ - white-space: normal; /* 1 */ -} - -/** - * Remove the default vertical scrollbar in IE. - */ - -textarea { - overflow: auto; -} - -/** - * 1. Add the correct box sizing in IE 10-. - * 2. Remove the padding in IE 10-. - */ - -[type="checkbox"], -[type="radio"] { - box-sizing: border-box; /* 1 */ - padding: 0; /* 2 */ -} - -/** - * Correct the cursor style of increment and decrement buttons in Chrome. - */ - -[type="number"]::-webkit-inner-spin-button, -[type="number"]::-webkit-outer-spin-button { - height: auto; -} - -/** - * 1. Correct the odd appearance in Chrome and Safari. - * 2. Correct the outline style in Safari. - */ - -[type="search"] { - -webkit-appearance: textfield; /* 1 */ - outline-offset: -2px; /* 2 */ -} - -/** - * Remove the inner padding and cancel buttons in Chrome and Safari on OS X. - */ - -[type="search"]::-webkit-search-cancel-button, -[type="search"]::-webkit-search-decoration { - -webkit-appearance: none; -} - -/** - * Correct the text style of placeholders in Chrome, Edge, and Safari. - */ - -::-webkit-input-placeholder { - color: inherit; - opacity: 0.54; -} - -/** - * 1. Correct the inability to style clickable types in iOS and Safari. - * 2. Change font properties to `inherit` in Safari. - */ - -::-webkit-file-upload-button { - -webkit-appearance: button; /* 1 */ - font: inherit; /* 2 */ -} - -/*! -Pure v0.6.0 -Copyright 2014 Yahoo! Inc. All rights reserved. -Licensed under the BSD License. -https://github.com/yahoo/pure/blob/master/LICENSE.md -*/ - -/*csslint important:false*/ - -/* ========================================================================== - Pure Base Extras - ========================================================================== */ - -/** - * Extra rules that Pure adds on top of Normalize.css - */ - -/** - * Always hide an element when it has the `hidden` HTML attribute. - */ - -.hidden, -[hidden] { - display: none !important; -} - -button, -[type="button"], -[type="reset"], -[type="submit"], -.button { - /* Structure */ - display: inline-block; - zoom: 1; - line-height: 1.25rem; - white-space: nowrap; - vertical-align: middle; - text-align: center; - -webkit-user-drag: none; - -webkit-user-select: none; - -moz-user-select: none; - -ms-user-select: none; - user-select: none; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -/* Firefox: Get rid of the inner focus border */ -button::-moz-focus-inner, -[type="button"]::-moz-focus-inner, -[type="reset"]::-moz-focus-inner, -[type="submit"]::-moz-focus-inner, -.button::-moz-focus-inner { - padding: 0; - border: 0; -} - -/*csslint outline-none:false*/ - -button, -[type="button"], -[type="reset"], -[type="submit"], -.button { - font-family: inherit; - font-size: 100%; - padding: 0.5em 1em; - color: #444; /* rgba not supported (IE 8) */ - color: rgba(0, 0, 0, 0.80); /* rgba supported */ - border: 1px solid #999; /*IE 6/7/8*/ - border: none rgba(0, 0, 0, 0); /*IE9 + everything else*/ - background-color: #E6E6E6; - text-decoration: none; - border-radius: 2px; -} - -button:hover, -button:focus, -[type="button"]:hover, -[type="button"]:focus, -[type="reset"]:hover, -[type="reset"]:focus, -[type="submit"]:hover, -[type="submit"]:focus, -.button.hover, -.button:hover, -.button:focus { - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(transparent), color-stop(40%, rgba(0,0,0, 0.05)), to(rgba(0,0,0, 0.10))); - background-image: -webkit-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); - background-image: -moz-linear-gradient(top, rgba(0,0,0, 0.05) 0%, rgba(0,0,0, 0.10)); - background-image: -o-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); - background-image: linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); -} -button:focus, -[type="button"]:focus, -[type="reset"]:focus, -[type="submit"]:focus, -.button:focus { - outline: 0; -} -button.active, -button:active, -[type="button"]:active, -[type="reset"]:active, -[type="submit"]:active, -.button.active, -.button:active { - box-shadow: 0 0 0 1px rgba(0,0,0, 0.15) inset, 0 0 6px rgba(0,0,0, 0.20) inset; - border-color: #000\9; -} - -button[disabled], -[type="button"][disabled], -[type="reset"][disabled], -[type="submit"][disabled], -.button[disabled], -.button.disabled, -.button.disabled:hover, -.button.disabled:focus, -.button.disabled:active { - border: none; - background-image: none; - opacity: 0.40; - cursor: not-allowed; - box-shadow: none; -} - -/*csslint box-model:false*/ -/* -Box-model set to false because we're setting a height on select elements, which -also have border and padding. This is done because some browsers don't render -the padding. We explicitly set the box-model for select elements to border-box, -so we can ignore the csslint warning. -*/ - -input[type="text"], -input[type="password"], -input[type="email"], -input[type="url"], -input[type="date"], -input[type="month"], -input[type="time"], -input[type="datetime"], -input[type="datetime-local"], -input[type="week"], -input[type="number"], -input[type="search"], -input[type="tel"], -input[type="color"], -input:not([type]), -select, -textarea { - padding: 0.4375em 0.6em; - display: inline-block; - border: 1px solid #ccc; - box-shadow: inset 0 1px 3px rgba(0, 0, 0, .13); - border-radius: 4px; - vertical-align: middle; - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} -input[type="text"], -input[type="password"], -input[type="email"], -input[type="url"], -input[type="date"], -input[type="month"], -input[type="time"], -input[type="datetime"], -input[type="datetime-local"], -input[type="week"], -input[type="number"], -input[type="search"], -input[type="tel"], -input[type="color"], -input:not([type]), -select { - height: 2.375em; -} - -/* Chrome (as of v.32/34 on OS X) needs additional room for color to display. */ -/* May be able to remove this tweak as color inputs become more standardized across browsers. */ -input[type="color"] { - padding: 0.2em 0.5em; -} - -input[type="text"]:focus, -input[type="password"]:focus, -input[type="email"]:focus, -input[type="url"]:focus, -input[type="date"]:focus, -input[type="month"]:focus, -input[type="time"]:focus, -input[type="datetime"]:focus, -input[type="datetime-local"]:focus, -input[type="week"]:focus, -input[type="number"]:focus, -input[type="search"]:focus, -input[type="tel"]:focus, -input[type="color"]:focus, -select:focus, -textarea:focus { - outline: 0; - border-color: #129FEA; -} - -/* -Need to separate out the :not() selector from the rest of the CSS 2.1 selectors -since IE8 won't execute CSS that contains a CSS3 selector. -*/ -input:not([type]):focus { - outline: 0; - border-color: #129FEA; -} - -.checkbox, -.radio { - margin: 0.5em 0; - display: block; -} - -input[type="text"][disabled], -input[type="password"][disabled], -input[type="email"][disabled], -input[type="url"][disabled], -input[type="date"][disabled], -input[type="month"][disabled], -input[type="time"][disabled], -input[type="datetime"][disabled], -input[type="datetime-local"][disabled], -input[type="week"][disabled], -input[type="number"][disabled], -input[type="search"][disabled], -input[type="tel"][disabled], -input[type="color"][disabled], -input[type="file"][disabled], -select[disabled], -textarea[disabled] { - cursor: not-allowed; - color: #cad2d3; -} -input[type="text"][disabled], -input[type="password"][disabled], -input[type="email"][disabled], -input[type="url"][disabled], -input[type="date"][disabled], -input[type="month"][disabled], -input[type="time"][disabled], -input[type="datetime"][disabled], -input[type="datetime-local"][disabled], -input[type="week"][disabled], -input[type="number"][disabled], -input[type="search"][disabled], -input[type="tel"][disabled], -input[type="color"][disabled], -select[disabled], -textarea[disabled] { - background-color: #eaeded; -} - -/* -Need to separate out the :not() selector from the rest of the CSS 2.1 selectors -since IE8 won't execute CSS that contains a CSS3 selector. -*/ -input:not([type])[disabled] { - cursor: not-allowed; - background-color: #eaeded; - color: #cad2d3; -} -input[readonly], -select[readonly], -textarea[readonly] { - background-color: #eee; /* menu hover bg color */ - color: #777; /* menu text color */ - border-color: #ccc; -} - -input:focus:invalid, -textarea:focus:invalid, -select:focus:invalid { - color: #b94a48; - border-color: #e9322d; -} - -select { - padding-top: .375em; - padding-bottom: .375em; - background-color: white; -} -select[multiple] { - height: auto; -} -label { - margin: 0.5em 0 0.2em; -} -fieldset { - margin: 0; - padding: 0.35em 0 0.75em; - border: 0; -} -legend { - display: block; - width: 100%; - padding: 0.3em 0; - margin-bottom: 0.3em; - color: #333; - border-bottom: 1px solid #e5e5e5; -} - -.stacked input[type="text"], -.stacked input[type="password"], -.stacked input[type="email"], -.stacked input[type="url"], -.stacked input[type="date"], -.stacked input[type="month"], -.stacked input[type="time"], -.stacked input[type="datetime"], -.stacked input[type="datetime-local"], -.stacked input[type="week"], -.stacked input[type="number"], -.stacked input[type="search"], -.stacked input[type="tel"], -.stacked input[type="color"], -.stacked input[type="file"], -.stacked select, -.stacked label, -.stacked textarea { - display: block; - margin: 0.25em 0; -} - -/* -Need to separate out the :not() selector from the rest of the CSS 2.1 selectors -since IE8 won't execute CSS that contains a CSS3 selector. -*/ -.stacked input:not([type]) { - display: block; - margin: 0.25em 0; -} -.aligned input, -.aligned textarea, -.aligned select, -/* NOTE: help-inline is deprecated. Use .message-inline instead. */ -.aligned .help-inline, -.message-inline { - display: inline-block; - *display: inline; - *zoom: 1; - vertical-align: middle; -} -.aligned textarea { - vertical-align: top; -} - -/* Aligned Forms */ -.aligned .control-group { - margin-bottom: 0.5em; -} -.aligned .control-group label { - text-align: right; - display: inline-block; - vertical-align: middle; - width: 10em; - margin: 0 1em 0 0; -} -.aligned .controls { - margin: 1.5em 0 0 11em; -} - -/* Rounded Inputs */ -input.input-rounded, -.input-rounded { - border-radius: 2em; - padding: 0.5em 1em; -} - -/* Grouped Inputs */ -.group fieldset { - margin-bottom: 10px; -} -.group input, -.group textarea { - display: block; - padding: 10px; - margin: 0 0 -1px; - border-radius: 0; - position: relative; - top: -1px; -} -.group input:focus, -.group textarea:focus { - z-index: 3; -} -.group input:first-child, -.group textarea:first-child { - top: 1px; - border-radius: 4px 4px 0 0; - margin: 0; -} -.group input:first-child:last-child, -.group textarea:first-child:last-child { - top: 1px; - border-radius: 4px; - margin: 0; -} -.group input:last-child, -.group textarea:last-child { - top: -2px; - border-radius: 0 0 4px 4px; - margin: 0; -} -.group button { - margin: 0.35em 0; -} - -.input-1 { - width: 100%; -} -.input-2-3 { - width: 66%; -} -.input-1-2 { - width: 50%; -} -.input-1-3 { - width: 33%; -} -.input-1-4 { - width: 25%; -} - -/* Inline help for forms */ -/* NOTE: help-inline is deprecated. Use .message-inline instead. */ -.help-inline, -.message-inline { - display: inline-block; - padding-left: 0.3em; - color: #666; - vertical-align: middle; - font-size: 0.875em; -} - -/* Block help for forms */ -form p { - color: #666; - font-size: 0.875em; -} - -@media only screen and (max-width: 480px) { - [type="button"], - [type="reset"], - [type="submit"], - form button:not([type]) { - margin: 0.7em 0 0; - } - - input:not([type]), - input[type="text"], - input[type="password"], - input[type="email"], - input[type="url"], - input[type="date"], - input[type="month"], - input[type="time"], - input[type="datetime"], - input[type="datetime-local"], - input[type="week"], - input[type="number"], - input[type="search"], - input[type="tel"], - input[type="color"], - select, - textarea, - label { - margin-bottom: 0.3em; - display: block; - } - - .group input:not([type]), - .group input[type="text"], - .group input[type="password"], - .group input[type="email"], - .group input[type="url"], - .group input[type="date"], - .group input[type="month"], - .group input[type="time"], - .group input[type="datetime"], - .group input[type="datetime-local"], - .group input[type="week"], - .group input[type="number"], - .group input[type="search"], - .group input[type="tel"], - .group input[type="color"] { - margin-bottom: 0; - } - - .aligned .control-group label { - margin-bottom: 0.3em; - text-align: left; - display: block; - width: 100%; - } - - .aligned .controls { - margin: 1.5em 0 0 0; - } - - /* NOTE: help-inline is deprecated. Use .message-inline instead. */ - .help-inline, - .message-inline, - .message, - .explanation { - display: block; - font-size: 0.75em; - /* Increased bottom padding to make it group with its related input element. */ - padding: 0.2em 0 0.8em; - } -} - -/*csslint adjoining-classes: false, box-model:false*/ -.menu { - -webkit-box-sizing: border-box; - -moz-box-sizing: border-box; - box-sizing: border-box; -} - -.menu-fixed { - position: fixed; - left: 0; - top: 0; - z-index: 3; -} - -.menu-list, -.menu-item { - position: relative; -} - -.menu-list { - list-style: none; - margin: 0; - padding: 0; -} - -.menu-item { - padding: 0; - margin: 0; - height: 100%; -} - -.menu-link, -.menu-heading { - display: block; - text-decoration: none; - white-space: nowrap; -} - -/* HORIZONTAL MENU */ -.menu-horizontal { - width: 100%; - white-space: nowrap; -} - -.menu-horizontal .menu-list { - display: inline-block; -} - -/* Initial menus should be inline-block so that they are horizontal */ -.menu-horizontal .menu-item, -.menu-horizontal .menu-heading, -.menu-horizontal .menu-separator { - display: inline-block; - *display: inline; - zoom: 1; - vertical-align: middle; -} - -/* Submenus should still be display: block; */ -.menu-item .menu-item { - display: block; -} - -.menu-children { - display: none; - position: absolute; - left: 100%; - top: 0; - margin: 0; - padding: 0; - z-index: 3; -} - -.menu-horizontal .menu-children { - left: 0; - top: auto; - width: inherit; -} - -.menu-allow-hover:hover > .menu-children, -.menu-active > .menu-children { - display: block; - position: absolute; -} - -/* Vertical Menus - show the dropdown arrow */ -.menu-has-children > .menu-link:after { - padding-left: 0.5em; - content: "\25B8"; - font-size: small; -} - -/* Horizontal Menus - show the dropdown arrow */ -.menu-horizontal .menu-has-children > .menu-link:after { - content: "\25BE"; -} - -/* scrollable menus */ -.menu-scrollable { - overflow-y: scroll; - overflow-x: hidden; -} - -.menu-scrollable .menu-list { - display: block; -} - -.menu-horizontal.menu-scrollable .menu-list { - display: inline-block; -} - -.menu-horizontal.menu-scrollable { - white-space: nowrap; - overflow-y: hidden; - overflow-x: auto; - -ms-overflow-style: none; - -webkit-overflow-scrolling: touch; - /* a little extra padding for this style to allow for scrollbars */ - padding: .5em 0; -} - -.menu-horizontal.menu-scrollable::-webkit-scrollbar { - display: none; -} - -/* misc default styling */ - -.menu-separator { - background-color: #ccc; - height: 1px; - margin: .3em 0; -} - -.menu-horizontal .menu-separator { - width: 1px; - height: 1.3em; - margin: 0 .3em ; -} - -.menu-heading { - text-transform: uppercase; - color: #565d64; -} - -.menu-link { - color: #777; -} - -.menu-children { - background-color: #fff; -} - -.menu-link, -.menu-disabled, -.menu-heading { - padding: .5em 1em; -} - -.menu-disabled { - opacity: .5; -} - -.menu-disabled .menu-link:hover { - background-color: transparent; -} - -.menu-active > .menu-link, -.menu-link:hover, -.menu-link:focus { - background-color: #eee; -} - -.menu-selected .menu-link, -.menu-selected .menu-link:visited { - color: #000; -} - -table { - /* Remove spacing between table cells (from Normalize.css) */ - border-collapse: collapse; - border-spacing: 0; - empty-cells: show; - border: 1px solid rgba(0, 0, 0, .1); -} -table + table { - margin-top: 1rem; -} - -caption { - font: italic 85%/1 'Arial', sans-serif; - text-align: center; - padding: 1em 0; -} - -td, -th { - border-left: 1px solid rgba(0, 0, 0, .1);/* inner column border */ - border-width: 0 0 0 1px; - font-size: inherit; - margin: 0; - overflow: visible; /*to make ths where the title is really long work*/ - padding: 0.5em 1em; /* cell padding */ -} - -thead, -tfoot { - text-align: left; - background-color: rgba(0, 0, 0, .1); -} - -/* end Pure */ - -html { - font-size: medium; - line-height: 1.5; -} - -:lang(en) { - quotes: '“' '”' '‘' '’'; -} -:lang(nl) { - quotes: '„' '”' '‚' '’'; -} - -q { - quotes: none; -} -q:before, -blockquote:before { - content: open-quote; -} -q:after, -blockquote:after { - content: close-quote; -} - -mark { - padding: .125em 0; -} - -/* input range */ -input[type="range"] { - width: 100%; /* Specific width is required for Firefox. */ - padding: 0; /* Gecko */ -} - -:not(pre) > code { - color: #000; - padding: .0625em .25em; - border: 1px solid rgba(0, 0, 0, .12); - border-radius: .125em; - background-color: rgba(0, 0, 0, .02); -} diff --git a/uweb3/scaffold/base/static/css/layout.css b/uweb3/scaffold/base/static/css/layout.css deleted file mode 100644 index f3a456bc..00000000 --- a/uweb3/scaffold/base/static/css/layout.css +++ /dev/null @@ -1,1146 +0,0 @@ -/* This is the Underdark layout CSS. Its structure is based on SMACSS. See -https://smacss.com for more info. */ - -/* Colors */ -/* 0, 120, 231 */ - -/* Complimentary */ -/* 154, 96, 0 */ -/* 0, 80, 154 */ - -/* Triad */ -/* 15, 88, 154 */ -/* 235, 35, 23 */ - -/* 167, 180, 18 */ -/* 143, 154, 8 */ - -html { - font-family: Arial, sans-serif; -} -body { - color: #444; - line-height: 1.5; - background-color: #fff; -} -h1, -h2, -h3, -h4, -h5, -h6 { - line-height: 1.25; -} -h1 { - font-size: 1.75em; -} -h2 { - font-size: 1.375em; -} -h3 { - font-size: 1.125em; -} - -blockquote { - font-style: italic; - margin-left: 2.5rem; - margin-right: 2.5rem; -} - -/* title */ -abbr[title], -span[title] { - cursor: help; -} -span[title] { - text-decoration: underline dotted; -} - -/* list */ -ol, -ul { - padding-left: 2.5rem; -} - -/* description list */ -dt { - font-weight: bold; -} -dd { - margin-left: 2.5rem; -} - -/* monospace */ -pre { - color: #000; - padding: 1rem; - border: 1px solid rgba(0, 0, 0, .12); - background-color: rgba(0, 0, 0, .02); - white-space: pre-wrap; -} -table pre { - margin: 0; - padding: 0; - border: 0; - background-color: transparent; -} -table code { - padding: 0; - border: 0; - border-radius: 0; - background-color: transparent; -} -kbd { - text-align: center; - text-transform: uppercase; - padding: .125rem .375rem; - border-bottom: .125rem solid #ccc; - border-radius: .25rem; - background-color: #f2f2f2; -} - -/* figure */ -figure { - display: inline-block; - padding: .5rem; - border: 1px solid #eee; -} -figcaption { - font-size: .875em; - font-style: italic; -} - -/* table */ -table { - width: 100%; -} -table + form, -div.scrollable + form { /* assumes scrollable contains table */ - border-top: 0; -} -td a, -th a { - display: inline-block; /* increase click area */ -} - -table ol, -table ul { - padding-left: 1rem; -} - -form table { - margin: 0 0 .5rem; -} -form + form { - border-top: 0; -} - -/* form single div */ -/* form > div:only-of-type > [type="submit"], -form fieldset > div:only-of-type > [type="submit"] { - margin: 0; -} */ -@media (min-width: 30rem) { - form input ~ input, - form input ~ select, - form select ~ input, - form select ~ select { - margin-left: 1rem; - } - - form div > label ~ button, - form div > label ~ [type="button"], - form div > label ~ [type="reset"], - form div > label ~ [type="submit"], - form div > label ~ .button { - margin: .0625rem 0 .0625rem 1rem; - } - form > div:only-of-type, /* don't put the submit outside the form control block */ - form fieldset > div:only-of-type { - margin: 0; - } - form > div:only-of-type > [type="submit"], - form fieldset > div:only-of-type > [type="submit"] { - min-width: auto; /* set default min-width, otherwise flexbox won't let the button grow when the value text is too long */ - } -} -form > div + [type="submit"], -form fieldset > div + [type="submit"] { - margin-top: .5rem; -} - -/*form > table { - margin-top: .25rem; -}*/ -th, -thead td, -tfoot td { - padding: .5rem 1rem; -} - -/* form */ -form { - padding: 1rem; - border: 1px solid #e5e5e5; - background-color: #f7f7f7; - box-sizing: border-box; -} -td > form, -th > form, -body > header form, -body > footer form { - padding: 0; - border: 0; - background-color: transparent; -} -body > header form { - font-size: .75rem; -} -body > header form.login, -body > header form.search { - margin-top: .5rem; -} - -body > header [type="submit"] { - min-width: 4rem; -} - -td > form, -th > form, -section > footer > form { - display: inline-block; -} - -/* input */ -input, -select, -textarea { - line-height: 1.25; - -webkit-transition: border-color .3s; - transition: border-color .3s; -} - -/* textarea */ -textarea { - max-width: 100%; - height: 10rem; - min-height: 4rem; -} -table textarea, -form > textarea { - vertical-align: top; -} -form fieldset > textarea { - vertical-align: baseline; -} - -/* button */ -button, -[type="button"], -[type="reset"], -[type="submit"], -.button { - color: #000; - min-width: 8rem; - max-width: 100%; -} -a.button:hover { - text-decoration: none; -} - -/* button in table */ -table button, -table [type="button"], -table [type="reset"], -table [type="submit"], -table .button { - font-size: .875em; - min-width: 4rem; - line-height: 1rem; - padding: .25rem .5rem; - vertical-align: top; -} - -/* form > button:not(:only-child), -form > [type="button"]:not(:only-child), -form > [type="reset"]:not(:only-child), -form > [type="submit"]:not(:only-child) { - margin-top: .5rem; -} -form > button:only-child, -form > [type="button"]:only-child, -form > [type="reset"]:only-child, -form > [type="submit"]:only-child { - margin-top: 0; -} */ - -form > div, -form fieldset > div { - margin: 0 0 .5rem; -} - -form div > input[type="number"] + span, -form div > input[type="range"] + span { - min-width: 24%; -} - -/* containers */ -body > header > div, -body > footer > div > nav, -body > footer > div.copyright p, -body > :not(nav) + main section, -body > :not(nav) + main :not(section) > .magazine, -body > :not(nav) + main :not(section) > .newspaper { - max-width: 80rem; - margin: 0 auto; - padding: 0 5%; -} - -/* body nav */ -body > nav { - padding: 0 5%; -} -body > nav > ul { - list-style: none; -} - -/* main */ -main > section, -main :not(aside):not(nav) > section { - padding: 2.5rem 5%; - word-wrap: break-word; -} -/* main section header */ -main section > header { - margin-bottom: 2.5rem; -} -main section > header > h1 { - margin-bottom: 0; -} -main section > header > h1 + p { - font-size: 1.125em; - margin-top: 0; -} -/* main section footer */ -main section > footer { - margin-top: 2.5rem; -} -main section > footer a { - display: inline-block; /* for images */ - max-width: 100%; /* for images */ -} -main section > footer > a > img { - display: block; - max-width: 100%; - height: auto; -} - -/* main sidebar */ -main > aside, -main > nav { - padding: 1rem 5%; -} -main > aside > ul, -main > nav > ul { - padding: 0; - list-style: none; -} -main aside > form, -main aside > nav, -main aside > section { - margin-bottom: 1em; - border: 1px solid #e2e2e2; - background-color: #f4f4f4; -} -main aside > section, -main aside > nav { - padding: 0 1em; -} -main aside > section > a, -main aside > nav > a { - display: inline-block; - margin-bottom: 1em; -} -main aside > section > form { - margin: 1em 0; - padding: 0; - border: 0; - background-color: transparent; -} - -main aside form, -main nav form { - padding-left: 1rem; - padding-right: 1rem; -} -main aside form > div, -main aside form fieldset > div, -main nav form > div, -main nav form fieldset > div { - display: block; /* cancel flexbox */ -} -main aside [type="submit"], -main nav [type="submit"] { - margin-left: 0; -} - -main aside legend { - color: inherit; -} - -body > nav + main { - padding: 0 5%; -} - -/* magazine and newspaper */ -main > .magazine, -main > .newspaper { - padding-left: 5%; - padding-right: 5%; -} -.magazine, -.newspaper { - column-gap: 3rem; - -moz-column-gap: 3rem; - -webkit-column-gap: 3rem; -} -.magazine { - columns: 20rem 2; - -moz-columns: 20rem 2; - -webkit-columns: 20rem 2; -} -.newspaper { - columns: 12rem 3; - -moz-columns: 12rem 3; - -webkit-columns: 12rem 3; -} -.magazine > *, -.newspaper > * { - display: inline-block; - width: 100%; -} -.magazine > section, -.newspaper > section { - padding: 0; -} -main section > header + .magazine > * > :first-child, -main section > header + .newspaper > * > :first-child { - /*margin-top: 0;*/ -} -main aside ~ div > .magazine, -main aside ~ div > .newspaper, -main > nav ~ div > .magazine, -main > nav ~ div > .newspaper { - padding: 0 5%; -} - -/* .pure-img */ -main > section > img { - display: block; - max-width: 100%; - height: auto; -} - -/* form */ -form ol, -form ul { - /*margin: 0 0 -.5rem 0;*/ - margin: .4375rem 0 .5rem; - padding: 0; - list-style: none; -} -form > div > p, -form fieldset > div > p { - margin-top: .5625rem; -} - -/* form li:not(:last-child) { - margin-bottom: .25rem; -} */ -/*form :not(fieldset) > ol > li, -form :not(fieldset) > ul > li { - display: inline-block; - margin-right: .5rem; -}*/ -form li > input[type="checkbox"] + label, -form li > input[type="radio"] + label { - display: inline; -} - -form :not(label):not(li) > input:not([type]), -form :not(label):not(li) > input[type="text"], -form :not(label):not(li) > input[type="password"], -form :not(label):not(li) > input[type="email"], -form :not(label):not(li) > input[type="url"], -form :not(label):not(li) > input[type="date"], -form :not(label):not(li) > input[type="month"], -form :not(label):not(li) > input[type="time"], -form :not(label):not(li) > input[type="datetime"], -form :not(label):not(li) > input[type="datetime-local"], -form :not(label):not(li) > input[type="week"], -form :not(label):not(li) > input[type="number"], -form :not(label):not(li) > input[type="search"], -form :not(label):not(li) > input[type="tel"], -form :not(label):not(li) > input[type="color"], -form :not(label):not(li) > input[type="file"], -form :not(label):not(li) > select, -form :not(label):not(li) > textarea { - width: 100%; - min-width: 0; -} - -form :not(label):not(li) > input[type="range"] { - height: 1.5rem; - margin-top: .4375rem; -} - -/* body header and footer */ -body > header, -body > footer { - font-size: .9375rem; -} -body > header nav ul, -body > footer nav ul { - padding: 0; - list-style: none; -} -body > header nav ul:not(.external) > li > a, -body > header div.logo > a { - text-decoration: none; -} - -/* body header */ -body > header { - color: #fff; - left: 0; - top: 0; - width: 100%; - height: 4.25rem; - border-bottom: .375rem solid #888; - background-color: #333; - overflow: hidden; - z-index: 1; - -webkit-tap-highlight-color: transparent; -} -body > header nav ul { - margin: 0; -} - -/* logo */ -body > header > div.logo { - /* the logo sometimes is a direct descendant of body header */ - padding: 0; -} -body > header div.logo > a { - font-size: 2rem; - font-weight: bold; - display: inline-block; /* decrease click area width */ - line-height: 2rem; - vertical-align: top; -} -body > header div.logo > a:only-child { - margin: .75rem 0; -} -body > header div.logo > p { - line-height: 1.5rem; /* round line-height */ - margin: 0; -} -@media screen and (max-width: 39.9375rem) { - body > header div.logo + nav { - padding-top: .375rem; - } -} - -/* menu toggle */ -body > header button.toggle { - text-indent: -9999rem; - position: absolute; - right: 5%; - top: 1rem; - width: 3rem; - min-width: 0; - height: 2.25rem; - border-radius: .125rem; - background: url('data:image/svg+xml,') center / 1rem no-repeat transparent; -} - -/* body header form */ -body > header > div > form > div { - margin-bottom: 0; -} -@media screen and (min-width: 40rem) { - body > header > div > form > div { - display: inline-block; - vertical-align: bottom; - } -} - -/* body header search */ -@media screen and (max-width: 39.9375rem) { - body > header > div > form + nav { - margin-top: .75rem; - } -} - -body > header > div > form + nav > ul.external { - margin: .875rem 0; -} - -@media screen and (max-width: 39.9375rem) { - body > header nav::after { - /* clearfix */ - content: ''; - display: table; - clear: right; - } -} - -/* external nav links */ -body > header nav > ul.external { - font-size: .875rem; - float: right; - margin: .375rem 0; -} -body > header nav > ul.external > li { - display: inline-block; -} -body > header nav > ul.external > li:not(:last-child) { - margin-right: 1em; -} -body > header nav > ul.external > li > a { - color: #888; -} - -/* site nav links */ -body > header nav > ul:not(.external) { - clear: right; - line-height: 1.25rem; /* round line-height */ -} -/* links and buttons */ -body > header nav > ul:not(.external) a, -body > header nav > ul:not(.external) > li > button { - width: 100%; - padding: .375rem 1em; - -webkit-transition: background-color .15s; - transition: background-color .15s; -} -body > header nav > ul:not(.external) a { - color: #fff; - display: block; - box-sizing: border-box; -} -body > header nav > ul:not(.external) a:focus, -body > header nav > ul:not(.external) > li > button:focus { - outline: 0; -} -body > header nav > ul:not(.external) a > i.fa { - /* equivalent of Font Awesome's fa-fw class */ - width: 1.28571429em; - text-align: center; -} -body > header nav > ul:not(.external) [type="submit"] { - margin-top: 0; - width: 100%; -} -@media screen and (min-width: 30rem) { - body > header nav > ul:not(.external) [type="submit"] { - margin-left: 0; /* reset */ - } -} -@media screen and (min-width: 40rem) { - body > header nav > ul:not(.external) [type="submit"] { - position: static; /* reset body > header [type="submit"] */ - line-height: inherit; - padding-top: .4375rem; - vertical-align: baseline; - } -} -/* top level links and buttons */ -body > header nav > ul:not(.external) > li > a, -body > header nav > ul:not(.external) > li > button { - border-radius: .125rem; - background-color: #444; - box-shadow: 0 1px 0 rgba(255, 255, 255, .2) inset; - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(transparent), color-stop(40%, rgba(0,0,0, 0.05)), to(rgba(0,0,0, 0.10))); - background-image: -webkit-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); - background-image: -moz-linear-gradient(top, rgba(0,0,0, 0.05) 0%, rgba(0,0,0, 0.10)); - background-image: -o-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); - background-image: linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); -} -body > header nav > ul:not(.external) > li > a:hover, -body > header nav > ul:not(.external) > li > a:focus, -body > header nav > ul:not(.external) > li > button:hover, -body > header nav > ul:not(.external) > li > button:focus { - background-color: #555; -} -body > header nav > ul:not(.external) > li > button { - color: inherit; - min-width: 0; - line-height: inherit; - vertical-align: baseline; -} -body > header nav > ul:not(.external) > li > button::after { - content: url('data:image/svg+xml,'); - display: inline-block; - width: .5rem; - height: 1.25rem; - margin-left: .25rem; - vertical-align: top; - opacity: .6; -} -body > header nav > ul:not(.external) > li.is-open > button { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; - background-color: #555; -} - -/* sub links */ -body > header nav > ul:not(.external) > li > ul { - height: 0; - overflow: hidden; - -webkit-transition: height ease-in-out .2s; - transition: height ease-in-out .2s; -} -body > header nav > ul:not(.external) > li > ul > li > a { - background-color: #555; -} -body > header nav > ul:not(.external) > li > ul > li > a:hover, -body > header nav > ul:not(.external) > li > ul > li > a:focus { - background-color: #666; -} - -/* main */ - -/* inputs may have container divs */ -body > header > div > form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -body > header > div > form select, -body > header > div > form textarea, -main aside form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -main aside form select, -main aside form textarea { - padding-top: .375rem; - padding-bottom: .375rem; -} -body > header > div > form > [type="submit"], -main aside form > [type="submit"] { - line-height: normal; - padding-top: .375rem; - padding-bottom: .375rem; -} - -main > .magazine form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -main > .magazine form select, -main > .magazine form label, -main > .magazine form textarea, -main > .newspaper form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -main > .newspaper form select, -main > .newspaper form label, -main > .newspaper form textarea, -main aside form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -main aside form select, -main aside form label, -main aside form textarea, -main > nav form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -main > nav form select, -main > nav form label, -main > nav form textarea, -body > header form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]), -body > header form select, -body > header form label, -body > header form textarea { - display: block; - width: 100%; - margin: .25rem 0; -} - -/* body group */ -body > div.group { - display: flex; -} -body > div.group > nav { - color: #fff; - background-color: #444; -} -body > div.group > nav a { - color: #fff; - text-decoration: none; - display: block; - padding: 1rem; - -webkit-transition: background-color 0.2s; - transition: background-color 0.2s; -} -body > div.group > nav a:hover { - background-color: rgba(255, 255, 255, .1); -} -body > div.group > nav > h2 { - text-align: center; - margin: 0; -} -body > div.group > nav > ul { - margin: 0; - padding: 0; - list-style: none; -} -body > div.group > nav > ul > li > a { - border-bottom: 1px solid rgba(0, 0, 0, .5); -} -body > div.group > main { - -webkit-flex: 1 1 auto; - flex: 1 1 auto; - max-width: none; - margin-left: 0; - margin-right: 0; - padding: 0 2rem; -} - -/* body footer */ -body > footer { - background-color: #222; -} -body > footer > div { - padding-top: 2rem; - padding-bottom: 2rem; -} -body > footer nav { - padding-left: 5%; - padding-right: 5%; -} -body > footer nav:not(.magazine):not(.newspaper) > ul { - text-align: center; -} -body > footer nav:not(.magazine):not(.newspaper) > ul > li { - display: inline-block; - margin: .25rem 1rem; -} -body > footer nav > ul.external > li > a:not(:hover) { - color: #777; -} -/* copyright */ -body > footer > div.copyright { - color: #666; - padding: .5rem 0; - background-color: #333; -} - -@media screen and (min-width: 30rem) { - form fieldset > div > textarea { - min-width: 62%; - max-width: 62%; - } - form > div > output, - form > div > span, - form fieldset > div > output, - form fieldset > div > span { - margin-top: .4375rem; - white-space: nowrap; - } - form > div > label + span:last-child, - form fieldset > div > label + span:last-child { - margin-top: 0; - } - - form div > label + span > input[type="text"], - form div > label + span > input[type="password"], - form div > label + span > input[type="email"], - form div > label + span > input[type="url"], - form div > label + span > input[type="date"], - form div > label + span > input[type="month"], - form div > label + span > input[type="time"], - form div > label + span > input[type="datetime"], - form div > label + span > input[type="datetime-local"], - form div > label + span > input[type="week"], - form div > label + span > input[type="number"], - form div > label + span > input[type="search"], - form div > label + span > input[type="tel"], - form div > label + span > input[type="color"], - form div > label + span > input:not([type]), - form div > label + span > select, - form div > label + span > textarea { - vertical-align: inherit; - } - - form > div > input + span, - form > div > select + span, - form > div > textarea + span, - form > div > output + span, - form fieldset > div > input + span, - form fieldset > div > select + span, - form fieldset > div > textarea + span, - form fieldset > div > output + span { - margin-left: .5rem; - } - /* first span after label */ - form > div > label + span:not(:last-child), - form fieldset > div > label + span:not(:last-child) { - margin-right: .5rem; - } - - form > div > span > label, - form > div > label + span > output, - form fieldset > div > span > label, - form fieldset > div > label + span > output { - display: inline-block; - margin-top: .4375rem; - } - form > div > span > input, - form > div > span > select, - form > div > span > textarea, - form > div > label + span > input, - form > div > label + span > select, - form > div > label + span > textarea { - width: auto; - } - - form > div, - form fieldset > div { - display: flex; - display: -webkit-flex; - align-items: flex-start; - -webkit-align-items: flex-start; - } - form > div > label:first-child, - form fieldset > div > label:first-child { - margin: .4375rem 0 0; - flex: 0 0 38%; - -webkit-flex: 0 0 38%; - } - form > div > input[type="file"], - form fieldset > div > input[type="file"] { - margin-top: .375em; - } - form ol, - form ul, - form input:not([type]), - form input[type="text"], - form input[type="password"], - form input[type="email"], - form input[type="url"], - form input[type="date"], - form input[type="month"], - form input[type="time"], - form input[type="datetime"], - form input[type="datetime-local"], - form input[type="week"], - form input[type="number"], - form input[type="search"], - form input[type="tel"], - form input[type="color"], - form input[type="file"], - form select, - form textarea, - form > div > label + div, - form fieldset > div > label + div { - flex: 0 1 62%; - -webkit-flex: 0 1 62%; - } - form > div + button, - form > [type="submit"], - form fieldset + button, - form > p, - form fieldset > p, - form > div > label:only-child, - form > div > span:only-child, - form fieldset > div > label:only-child, - form fieldset > div > span:only-child { - margin-left: 38%; - } - table [type="submit"], - form > [type="submit"]:only-child, - form > :not(div):not(fieldset) + [type="submit"] { - margin-left: 0; /* reset */ - } -} - -@media screen and (max-width: 39.9375rem) { - body > header { - position: absolute; - -webkit-transition: height ease-in-out .3s; - transition: height ease-in-out .3s; - } - body > header > div { - padding-top: .375rem; - padding-bottom: .375rem; - } - body > header nav > ul:not(.external) > li:not(:last-child) { - margin-bottom: .1875rem; - } - - main { - padding-top: 4.625rem; - } -} -@media screen and (min-width: 40rem) { - h1 { - font-size: 2.375em; - } - - body > nav { - float: left; - padding-right: 0; - } - body > nav + main { - margin-left: 20%; - } - - main > aside, - main > nav, - section > aside { - float: left; - width: 24%; - padding-left: 3%; - } -/* body > :not(header) :not(main) > aside, - body > :not(header) :not(main) > nav { - padding-right: 3%; - } */ - main > aside, - main > nav { - padding-right: 0; - } - main section > aside { - padding-left: 3%; - padding-right: 0; - padding-bottom: 0; - } - - main > nav form input, - main > nav form button:not([type]), - main > nav form [type="submit"] { - min-width: 0; - max-width: 100%; - } - main aside ~ div, - main > nav ~ div, - main > nav ~ section { - margin-left: 27% - } -/* main aside ~ div > section, - main > nav ~ div > section { - padding-left: 0; - } */ - -/* main aside ~ div > .magazine, - main aside ~ div > .newspaper, - main > nav ~ div > .magazine, - main > nav ~ div > .newspaper { - padding-left: 0; - } */ - - form > div > textarea { - /* only give textarea min width, since they can be resized */ - min-width: 62%; - } - - form fieldset > div > .input-1-2 { - width: 38%; - margin-right: 1%; - } - form fieldset > div > .input-1-2:last-child { - margin-right: 0; - } - - form fieldset > div > [type="submit"] { - margin-left: 38%; - } - - body > header { - min-height: 8rem; /* in case JS sets height to 0 */ - max-height: 8rem; - } - body > header div.logo { - float: left; - margin-top: 2.25rem; - } - - body > header > div > form { - float: right; - margin-bottom: 1.625rem; - } - body > header [type="submit"] { - /* position: relative; */ - /* top: 1.625rem; */ - margin-left: 0 !important; /* TODO: fix weight of :not(aside):not(nav):not(.magazine):not(.newspaper) > :not(td):not(th) > form > div + [type="submit"] */ - margin-bottom: .3125rem; - } - - body > header nav { - clear: right; /* in case float right previous form */ - } - body > header nav > ul.external { - margin: 2.25rem 0; - line-height: 1.5rem; /* round line-height */ - } - body > header > div > :not(form) + nav > ul:not(.external):only-child { - clear: left; /* clear logo */ - padding-top: .25rem; - } - body > header > div > :not(form) + nav > ul.external:only-child { - margin: 3rem 0; - } - - body > header nav > ul:not(.external) { - text-align: right; - } - body > header nav > ul:not(.external) > li { - text-align: left; - display: inline-block; - } - - body > header > div > form + nav > ul:not(.external):only-child { - margin-top: 2rem; - } - body > header nav > ul:not(.external) > li > ul { - position: absolute; - } - body > header nav > ul:not(.external) > li > a, - body > header nav > ul:not(.external) > li > button { - border-bottom-right-radius: 0; - border-bottom-left-radius: 0; - } - body > header nav > ul ul { - border-bottom: .375rem solid #888; - } - - body > header button.toggle { - display: none; - } - - body > footer { - clear: both; - } -} - -@media screen and (min-width: 60rem) { - form { - padding-left: 2rem; - padding-right: 2rem; - } - -/* main > section, - main :not(aside):not(nav) > section, - main aside ~ div > .magazine, main aside ~ div > .newspaper, main > nav ~ div > .magazine, main > nav ~ div > .newspaper { - padding-left: 6%; - padding-right: 6%; - } */ - main > aside { - padding-right: 0; - } - .magazine, - .newspaper { - column-gap: 4rem; - -moz-column-gap: 4rem; - -webkit-column-gap: 4rem; - } -} - -/* trimmable - useful for e.g. long key codes that do not get trimmed somewhere else */ -td.trimmable { - max-width: 4rem; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; -} - -td.data { - word-break: break-all; -} diff --git a/uweb3/scaffold/base/static/css/module.css b/uweb3/scaffold/base/static/css/module.css deleted file mode 100644 index bdb80f81..00000000 --- a/uweb3/scaffold/base/static/css/module.css +++ /dev/null @@ -1,710 +0,0 @@ -/* This is the Underdark module CSS. Its structure is based on SMACSS. See -https://smacss.com for more info. */ - -/* ssh key */ -textarea.sshkey, -pre.sshkey { - word-break: break-all; -} -textarea.sshkey { - font-family: monospace, monospace; -} - -/* table */ -td.number, -th.number { - text-align: right; -} -td.number input { - text-align: inherit; -} - -/* sortable */ -th.sortable > a, -th.ascending > a, -th.descending > a { - color: transparent; - display: inline-block; - width: 1.5rem; - height: 1.5rem; - white-space: nowrap; - overflow: hidden; - vertical-align: top; -} -th.sortable > a::before { - content: url('data:image/svg+xml,'); -} -th.ascending > a::before { - content: url('data:image/svg+xml,'); -} -th.descending > a::before { - content: url('data:image/svg+xml,'); -} - -/* pros and cons list */ -ul.pros, -ul.cons { - font-size: .9375rem; - color: #555; - line-height: 1.6875rem; - margin: 1rem 0; - padding: .5rem 1rem .5rem 2.5rem; - background-color: #f7f7f7; - list-style: none; -} -ul.pros > li, -ul.cons > li { - position: relative; - margin: .5rem 0; -} -ul.pros > li > .fa, -ul.cons > li > .fa { - font-size: 1rem; - position: absolute; - left: -1.5rem; - width: 1.125rem; - text-align: center; - line-height: 1.6875rem; -} - -/* pagination */ -nav.pagination { - text-align: center; -} -nav.pagination > ol { - padding: 0; -} -nav.pagination > ol > li { - display: inline-block; -} -/* nav.pagination > ol > li:not(:last-child) { - margin-right: .5rem; -} */ -nav.pagination > ol > li { - margin: .125rem 0; -} -nav.pagination > ol > li.active, -nav.pagination > ol > li > a { - padding: .25rem .75rem; -} -nav.pagination > ol > li > a { - display: block; - border: 1px solid rgb(0, 0, 0, .1); -} - -/* form */ -form.is-submitting [type="submit"], -form.is-submitting button:not([type]) { - opacity: .2; - cursor: default; -} - -/* toggle */ -input[type="checkbox"].toggle { - opacity: 0; -} -input[type="checkbox"].toggle + label { - text-indent: -9999em; - display: inline-block; - position: relative; - left: -1em; - width: 3em; - height: 1.5em; - border-radius: .75em; - background-color: rgb(235, 35, 23); - overflow: hidden; - user-select: none; - -ms-user-select: none; - -moz-user-select: none; - -webkit-user-drag: none; - -webkit-user-select: none; - transition: background-color .2s; - -webkit-transition: background-color .2s; -} -input[type="checkbox"].toggle:checked + label { - background-color: rgb(35, 235, 23); -} -input[type="checkbox"].toggle[disabled] + label { - opacity: .4; - cursor: not-allowed; -} -input[type="checkbox"].toggle + label::after { - content: ''; - position: absolute; - left: .125em; - top: .125em; - width: 1.25em; - height: 1.25em; - border-radius: 50%; - background-color: #fff; - transition: transform .2s; - -webkit-transition: -webkit-transform .2s; -} -input[type="checkbox"].toggle:checked + label::after { - transform: translateX(1.5em); - -webkit-transform: translateX(1.5em); -} -input[type="checkbox"].toggle:not([disabled]):hover + label::after, -input[type="checkbox"].toggle:not([disabled]):focus + label::after { - background-image: -webkit-gradient(linear, 0 0, 0 100%, from(transparent), color-stop(40%, rgba(0,0,0, 0.05)), to(rgba(0,0,0, 0.10))); - background-image: -webkit-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); - background-image: -moz-linear-gradient(top, rgba(0,0,0, 0.05) 0%, rgba(0,0,0, 0.10)); - background-image: -o-linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); - background-image: linear-gradient(transparent, rgba(0,0,0, 0.05) 40%, rgba(0,0,0, 0.10)); -} -input[type="checkbox"].toggle:active:not([disabled]):hover + label::after, -input[type="checkbox"].toggle:active:not([disabled]):focus + label::after { - background-color: #eee; -} - -/* editable */ -div.editable { - position: relative; - overflow: hidden; - transition: height .5s; - -webkit-transition: height .5s; -} -div.editable > section { - position: absolute; - left: 0; - top: 0; - width: 100%; - max-width: 100%; - box-sizing: border-box; - transition-duration: .5s; - -webkit-transition-duration: .5s; - transition-property: opacity, transform; - -webkit-transition-property: opacity, -webkit-transform; -} -div.editable.is-editing > section:first-child, -div.editable:not(.is-editing) > section:nth-child(2) { - opacity: 0; -} -div.editable.is-editing > section:first-child { - transform: translateX(-100%); - -webkit-transform: translateX(-100%); -} -div.editable:not(.is-editing) > section:nth-child(2) { - transform: translateX(100%); - -webkit-transform: translateX(100%); -} -div.editable > section button.edit { - font-size: .875rem; - /* position: absolute; - top: 1rem; - right: 5%; */ - float: right; - min-width: 4rem; - margin-top: 1rem; -} -div.editable > section button.view { - margin-top: .5rem; -} - -/* filter */ -div.filter { - text-align: right; -} - -/* search */ -body > header form.search > div:only-of-type > label, -body > footer form.search > div:only-of-type > label, -aside form.search > div:only-of-type > label, -nav form.search > div:only-of-type > label { - position: absolute; - visibility: hidden; -} -form.search > div > input { - flex-basis: auto; -} -/* search */ -/* TODO: Should be basic module style. Scoped to main for now */ -main form.search > div:only-of-type > input[type="search"] { - flex-basis: 100%; -} -main form.search > div:only-of-type > [type="submit"], -main form.search > div:only-of-type > .button { - min-width: 6rem; -} -main form.search > div:only-of-type > .button { - margin-left: .5rem; -} - -/* search in body header */ -body > header form.search > div:only-of-type > label { - position: static; /* reset */ -} - -@media screen and (min-width: 40rem) { - body > header > div > form.login + form.search { - margin-right: 1rem; - } -} - -/* form.search { - display: inline-block; - float: right; -} -main > div > section > header > form.search > div, -main > div > section > header > form.search > p { - display: inline-block; -} */ - -/* steps */ -ol.steps { - padding: 0; - list-style: none; -} -ol.steps > li { - display: inline-block; - position: relative; - margin: .125rem 0; - vertical-align: top; -} -ol.steps > li > label { - display: block; - position: relative; - margin: 0; /* TODO: Make default margin on labels way more specific and remove this */ - padding: 0 .5rem 0 1.75rem; - background-color: rgba(0, 0, 0, .1); -} -ol.steps > li:not(:last-child) { - margin-right: .5rem; -} - -ol.steps > li > input[type="checkbox"] { - position: absolute; - left: .5rem; - top: .3125rem; - z-index: 1; -} -ol.steps > li:not(:first-child) > input[type="checkbox"] + label::before, -ol.steps > li:not(:last-child) > input[type="checkbox"] + label::after { - position: absolute; - width: .5rem; - height: 1.5rem; -} -ol.steps > li:not(:first-child) > input[type="checkbox"] + label::before { - content: url('data:image/svg+xml,'); - left: -.5rem; - top: 0; -} -ol.steps > li:not(:last-child) > input[type="checkbox"] + label::after { - content: url('data:image/svg+xml,'); - right: -.5rem; - bottom: 0; -} - -ol.steps > li > input[type="checkbox"]:disabled, -ol.steps > li > input[type="checkbox"]:disabled + label { - position: absolute; - visibility: hidden; -} - -/* tabs */ -.tabs { - display: block !important; /* TODO: fix flexbox layout issues and selector specificity */ - border: 1px solid rgba(0, 0, 0, .05); - background-color: rgba(0, 0, 0, .05); -} -.tabs > input[type="radio"] { - display: none; -} - -.tabs > label { - float: left; - margin: 0 !important; /* TODO: fix selector specificity */ - padding: .5em 1em; - -webkit-transition: background-color .2s; - transition: background-color .2s; -} -.tabs > label:not(.is-active):focus, -.tabs > label:not(.is-active):hover { - background-color: rgba(255, 255, 255, .5); -} -.tabs > label.is-active { - color: inherit; - background-color: #fff; -} - -.tabs > div, -.tabs > section { - clear: left; /* label float */ - padding: 1em; - background-color: #fff; -} - -.tabs > input[type="radio"]:not(:checked) + div, -.tabs > input[type="radio"]:not(:checked) + section { - display: none; -} -/* tabs in table */ -table .tabs > label { - font-size: .875rem; -} -table .tabs > div, -table .tabs > section { - padding: .5em; -} - -/* toddler */ -main.toddler > section { - box-sizing: border-box; -} - - -@media screen and (min-width: 100rem) { - div.editable > section > button.edit { - right: 10%; - } -} - -/* modal */ -.modal { - display: flex; - align-items: center; - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - padding: 0 1rem; - z-index: 9999; - box-sizing: border-box; - -webkit-transition: opacity .2s; - transition: opacity .2s; -} -.modal label[for^="modaltoggle"] { - font-weight: bold; - text-align: center; - display: block; - margin: 0 -1rem; - padding: .5rem 1rem; - background-color: #eee; - -webkit-transition: background-color .1s; - transition: background-color .1s; -} -.modal label[for^="modaltoggle"]::before { - content: ''; - position: fixed; - top: 0; - right: 0; - bottom: 0; - left: 0; - background-color: rgba(0, 0, 0, .5); - z-index: -1; -} -.modal label[for^="modaltoggle"]::hover { - background-color: #ddd; -} -.modal > aside, -.modal > section { - max-width: 30rem; - margin: 0 auto; - padding: 0 1rem; - border-radius: .5rem; - background-color: #fff; - box-shadow: 0 .5rem 1rem rgba(0, 0, 0, .2); - overflow: hidden; -} -.modal > aside::before, -.modal > section::before { - font-size: 1rem; - font-weight: bold; - color: #fff; - display: block; - margin: 0 -1rem; - padding: .75rem 1rem .75rem 3.25rem; - background-position: 1rem center; - background-repeat: no-repeat; -} -/* modal type */ -.modal.success > aside::before, -.modal.success > section::before { - content: 'Success'; - background-color: hsl(103, 44%, 49%); - background-image: url('data:image/svg+xml,'); -} -.modal.info > aside::before, -.modal.info > section::before { - content: 'Information'; - background-color: hsl(200, 65%, 51%); - background-image: url('data:image/svg+xml,'); -} -.modal.warning > aside::before, -.modal.warning > section::before { - content: 'Warning'; - background-color: hsl(50, 81%, 54%); - background-image: url('data:image/svg+xml,'); -} -.modal.error > aside::before, -.modal.error > section::before { - content: 'Error'; - background-color: hsl(0, 43%, 51%); - background-image: url('data:image/svg+xml,'); -} -[lang="nl"] .modal.success > aside::before, -[lang="nl"] .modal.success > section::before { - content: 'Gelukt'; -} -[lang="nl"] .modal.info > aside::before, -[lang="nl"] .modal.info > section::before { - content: 'Informatie'; -} -[lang="nl"] .modal.warning > aside::before, -[lang="nl"] .modal.warning > section::before { - content: 'Waarschuwing'; -} -[lang="nl"] .modal.error > aside::before, -[lang="nl"] .modal.error > section::before { - content: 'Mislukt'; -} -/* modal state */ -.modaltoggle:not(:checked) + .modal { - opacity: 0; - pointer-events: none; -} - -/* messages block (-like) */ -p.primary, -tr.primary td, -tr.primary th, -td.primary, -th.primary, -button.primary, -input.primary, -mark.primary { - color: #fff; - background-color: hsl(208, 56%, 46%); -} -p.success, -tr.success td, -tr.success th, -td.success, -th.success, -input.success, -mark.success { - background-color: hsl(103, 44%, 89%); -} -p.info, -tr.info td, -tr.info th, -td.info, -th.info, -input.info, -mark.info { - background-color: hsl(200, 65%, 91%); -} -p.warning, -tr.warning td, -tr.warning th, -td.warning, -th.warning, -input.warning, -mark.warning { - background-color: hsl(50, 81%, 94%); -} -p.error, -tr.error td, -tr.error th, -td.error, -th.error, -input.error, -mark.error { - background-color: hsl(0, 43%, 91%); -} -/* messages p */ -p.primary, -p.success, -p.info, -p.warning, -p.error { - padding: 1em; -} -p.success::before, -p.info::before, -p.warning::before, -p.error::before { - display: inline-block; - width: 1.25em; - height: 1.25em; - margin-right: .5em; - vertical-align: middle; -} -p.success::before { - content: url('data:image/svg+xml,'); -} -p.info::before { - content: url('data:image/svg+xml,'); -} -p.warning::before { - content: url('data:image/svg+xml,'); -} -p.error::before { - content: url('data:image/svg+xml,'); -} -/* messages button */ -button.success, -input[type="button"].success { - color: #fff; - background-color: hsl(103, 44%, 49%); -} -button.info, -input[type="button"].info { - color: #fff; - background-color: hsl(200, 65%, 51%); -} -button.warning, -input[type="button"].warning { - color: #fff; - background-color: hsl(50, 81%, 54%); -} -button.error, -input[type="button"].error { - color: #fff; - background-color: hsl(0, 43%, 51%); -} -/* messages inline other */ -label.primary, -dd.primary { - color: hsl(208, 56%, 46%); -} -label.success, -dd.success { - color: hsl(103, 44%, 49%); -} -label.info, -dd.info { - color: hsl(200, 65%, 51%); -} -label.warning, -dd.warning { - color: hsl(50, 81%, 54%); -} -label.error, -dd.error { - color: hsl(0, 43%, 51%); -} - -dl.details > dt { - font-weight: bold; -} -dl.details > dd { - margin: 0 0 .5rem; -} - -@media screen and (min-width: 20rem) { - dl.details::after { - /* clearfix */ - content: ''; - display: table; - clear: left; - } - /* NOTE: Floating dts won't work with long terms */ - dl.details > dt { - clear: both; - float: left; - width: 38%; - margin-bottom: .5rem; - padding-right: 1rem; - box-sizing: border-box; - } - dl.details > dd { - clear: right; - float: right; - width: 62%; - } -} - -/* dl.details > dd::before { - content: '– '; -} */ -/* -dl.details { - display: -webkit-flex; - display: flex; - -webkit-flex-wrap: wrap; - flex-wrap: wrap; -} -dl.details > dt { - font-weight: bold; - width: 38%; -} -dl.details > dd { - width: 62%; - margin-left: 0; -} -dl.details > dt + dd { -} -dl.details > dd + dd { - margin-left: 38%; - -} -*/ - -/* vars */ -dl.vars > dt { - float: left; -} -dl.vars > dt::after { - content: ': '; - margin-right: .5rem; /* add margin because float eats the space */ -} -dl.vars > dt var { - font-style: inherit; -} -dl.vars > dd { - margin: 0; - word-break: break-all; -} - -/* tree */ -ul.tree, -ul.tree ul, -ul.tree li { - position: relative; -} -ul.tree { - list-style: none; -} -ul.tree ul { - list-style: none; - padding-left: 32px; -} -ul.tree li::before, -ul.tree li::after { - content: ""; - position: absolute; - left: -12px; -} -ul.tree li::before { - border-top: 2px solid #9b9b9b; - top: 11px; - width: 8px; -} -ul.tree li::after { - border-left: 2px solid #9b9b9b; - height: 100%; - padding-top: .5rem; - top: -.25rem; -} -ul.tree ul > li:last-child::after { - height: 8px; -} -ul.tree > li:last-child::after, -ul.tree > li:last-child::before { - display: none; -} - -/* scrollable (used for overflowing table) */ -div.scrollable { - overflow-x: auto; -} -div.scrollable + div.scrollable { - margin-top: 1rem; -} -@media screen and (min-width: 30rem) { - form > div.scrollable + [type="submit"] { - margin-left: 0; /* reset */ - } -} diff --git a/uweb3/scaffold/base/static/css/theme.css b/uweb3/scaffold/base/static/css/theme.css deleted file mode 100644 index 26cd970c..00000000 --- a/uweb3/scaffold/base/static/css/theme.css +++ /dev/null @@ -1,206 +0,0 @@ -/* This is your theme file. Use the selectors used in the other layers or extend them. -See https://css.underdark.nl/docs for further explanation */ - -/* Colours */ -/* 0, 120, 231 */ - -/* Complimentary */ -/* 154, 96, 0 */ -/* 0, 80, 154 */ - -/* Triad */ -/* 15, 88, 154 */ -/* 235, 35, 23 */ - -/* 167, 180, 18 */ -/* 143, 154, 8 */ - -a { - color: hsl(208, 56%, 46%); - -webkit-transition: color .2s; - transition: color .2s; -} -/* a:not(:hover) { - text-decoration: none; -} */ - -/* lvha */ -a:link { -} -a:visited { - /* color: rgb(118, 78, 127); */ -} -a:hover { - color: hsl(208, 56%, 66%); -} -a:active { - /* color: rgb(235, 35, 23); */ -} - -pre { - color: #000; -} -:not(pre) > code { - color: #000; -} - -label { - color: #888; -} - -figure > em { - color: #ccc; -} - -/* body header and footer */ -body > header, -body > header nav > ul ul { - border-color: hsl(208, 56%, 46%); -} - -body > header a:focus, -body > header a:hover, -body > footer a:focus, -body > footer a:hover { - text-shadow: 0 0 1em hsl(208, 56%, 46%); -} - -/* depth header and footer links */ -body > header nav > ul:not(.external) a, -body > header nav > ul:not(.external) > li > button { - text-shadow: 0 -1px 1px rgba(0, 0, 0, .5); -} - -body > header nav > ul.external > li > a, -body > header .logo > a, -body > footer a { - text-decoration: none; - -webkit-transition-duration: .2s; - transition-duration: .2s; - -webkit-transition-property: color, text-shadow; - transition-property: color, text-shadow; -} -body > header nav > ul.external > li > a:focus, -body > header .logo > a:focus, -body > footer a:focus { - outline: 0; -} -body > header nav > ul:not(.external) > li > button::after { - text-shadow: 0 1px 0 rgba(0, 0, 0, .5); -} - -/* table */ -table a { - /* increase click area size */ - /* display: inline-block; */ -} -thead, -tfoot { - color: #000; -} -/* caption { - color: #000; -} */ - -/* form */ -form p { - font-size: .875rem; - color: #666; -} - -main aside, -main nav { - /* TODO: should be more specific. aside and navs should be normaly usable inside sectioning elements */ - /* font-size: .875rem; */ - color: #666; -} - -/* button */ -/* [type="submit"], */ -[type="button"].primary, -[type="button"].selected, -button.primary, -button.selected, -a.button.primary, -a.button.selected { - background-color: hsl(208, 56%, 46%); - color: #fff; -} -a.button:hover { - color: #000; /* reset */ - /* text-decoration: none; */ -} -a.button.primary:hover, -a.button.selected:hover { - color: #fff; /* reset */ -} - -.logo > a { - -webkit-transition-property: color, text-shadow; - transition-property: color, text-shadow; -} - -/* sticky footer */ -body { - display: -webkit-flex; - display: flex; - min-height: 100vh; - -webkit-flex-direction: column; - flex-direction: column; -} -body > main { - -webkit-flex: 1; - flex: 1; - width: 100%; - box-sizing: border-box; -} - -/* - * When a user agent cannot parse the selector (i.e., it is not valid CSS 2.1), - * it must ignore the selector and the following declaration block (if any) as well. - * See: http://stackoverflow.com/questions/20541306/how-to-write-a-css-hack-for-ie-11 - */ -@media screen and (-ms-high-contrast: active), (-ms-high-contrast: none) { - /* Fix body collapsing in IE 11 when flexbox is used for sticky footer. */ - body { - display: block; /* reset */ - } -} - -/* pagination */ -nav.pagination > ol > li.first, -nav.pagination > ol > li.previous, -nav.pagination > ol > li.next, -nav.pagination > ol > li.last { - overflow: hidden; - white-space: nowrap; - vertical-align: bottom; /* overflow hidden changes vertical alignment */ -} -nav.pagination > ol > li.first > a, -nav.pagination > ol > li.previous > a, -nav.pagination > ol > li.next > a, -nav.pagination > ol > li.last > a { - color: transparent; - width: 1rem; -} -nav.pagination > ol > li.first > a::before, -nav.pagination > ol > li.previous > a::before, -nav.pagination > ol > li.next > a::before, -nav.pagination > ol > li.last > a::before { - display: inline-block; - width: 1rem; - height: 1.5rem; - vertical-align: top; -} -nav.pagination > ol > li.first > a::before { - content: url('data:image/svg+xml,'); -} -nav.pagination > ol > li.previous > a::before { - content: url('data:image/svg+xml,'); -} -nav.pagination > ol > li.next > a::before { - content: url('data:image/svg+xml,'); -} -nav.pagination > ol > li.last > a::before { - content: url('data:image/svg+xml,'); -} diff --git a/uweb3/scaffold/base/static/scripts/ajax.js b/uweb3/scaffold/base/static/scripts/ajax.js deleted file mode 100644 index 65f7fa35..00000000 --- a/uweb3/scaffold/base/static/scripts/ajax.js +++ /dev/null @@ -1,234 +0,0 @@ -var ud = ud || {}; - -ud.ajax = (function () { - 'use strict'; - var MAX_CONNECTIONS_PER_HOSTNAME = 6; - var request = { - id: 0, - type: 'update', - method: 'get', - url: '', - data: {}, - mimeType: 'application/json', - contentType: 'application/x-www-form-urlencoded; charset=UTF-8', - form: null, - hostname: '', - pollDelay: 3000, - xhr: {}, - - change: null, - success: null, - error: null, - - create: function (url, settings) { - var that = Object.create(this); - - that.url = url; - that.extend(settings); - that.setUpXhr(); - return that; - }, - - extend: function (props) { - for (var prop in props) { - if (props.hasOwnProperty(prop)) { - this[prop] = props[prop]; - } - } - }, - - setUpXhr: function () { - this.xhr = new window.XMLHttpRequest(); - if (this.xhr.overrideMimeType) { - this.xhr.overrideMimeType(this.mimeType); - } - this.xhr.onreadystatechange = this.handleReadyStateChange.bind(this); - }, - - send: function () { - var query = this.form ? this.getFormDataString() : this.getQueryString(); - var body = null; - - if (query) { - if (this.method === 'get') { - this.url += '?' + query; - } - if (this.method === 'post') { - body = query; - } - } - this.xhr.open(this.method, this.url, true); - this.xhr.setRequestHeader('Content-type', this.contentType); - this.xhr.setRequestHeader('X-CSRF-Token', csrf.token); - this.xhr.setRequestHeader('HTTP_X_REQUESTED_WITH', 'xmlhttprequest'); - this.xhr.send(body); - }, - - handleReadyStateChange: function () { - if (this.change) { - this.change(this); - } - if (this.xhr.readyState === 4) { - if(this.mimeType === 'application/json'){ - if (this.xhr.status === 200 && this.success) { - this.success(JSON.parse(this.xhr.responseText), this.url); - } else if (this.error) { - this.error(this.xhr); - } - }else{ - this.success(this.xhr.responseText, this.url) - } - if (this.type === 'update') { - this.remove(); - } - } - }, - - getHostname: function () { - var parser; - - if (!this.hostname) { - parser = document.createElement('a'); - parser.href = this.url; - this.hostname = parser.hostname; - } - return this.hostname; - }, - - getQueryString: function (data) { - var list = []; - - data = data || this.data; - for (var name in data) { - if (data[name] instanceof Array) { - data[name] = data[name].join(','); - } - list.push(window.encodeURIComponent(name) + '=' + - window.encodeURIComponent(data[name]) - ); - } - return list.join('&'); - }, - - getFormDataString: function () { - var els = this.form.elements; - var data = {}; - - for (var i = 0; i < els.length; i++) { - if (els[i].name) { - data[els[i].name] = els[i].value; - } - } - return this.getQueryString(data); - } - }; - - var overrides = { - active: {}, - - add: function (req) { - var active = this.active[req.url]; - - if (active && active.xhr.readyState !== 4) { - active.xhr.abort(); - } - req.send(); - this.active[req.url] = req; - } - }; - - var updates = { - active: {}, - queue: {}, - - add: function (req) { - var hostname = req.getHostname(); - var active = this.active[hostname] || []; - var queue = this.queue[hostname] || []; - - if (active.length < MAX_CONNECTIONS_PER_HOSTNAME) { - req.send(); - active.push(req); - } else { - queue.push(req); - } - req.remove = this.remove.bind(this, req); - this.active[hostname] = active; - this.queue[hostname] = queue; - }, - - remove: function (req) { - var hostname = req.getHostname(); - var active = this.active[hostname]; - var next = this.queue[hostname].shift(); - - if (next) { - next.send(); - active.push(next); - } - active.splice(active.indexOf(req), 1); - this.active[hostname] = active; - } - }; - - var polls = { - active: {}, - interval: {}, - - add: function (req) { - var interval = this.interval[req.url]; - - if (!interval) { - req.send(); - req.remove = this.remove.bind(this, req); - interval = window.setInterval(req.send.bind(req), req.pollDelay); - this.interval[req.url] = interval; - this.active[req.url] = req; - } - }, - - remove: function (req) { - var interval = this.interval[req.url]; - - window.clearInterval(interval); - delete this.active[req.url]; - } - }; - - var proxy = { - types: { - override: overrides, - update: updates, - poll: polls - }, - count: 0, - - add: function (req) { - req.id = ++this.count; - this.types[req.type].add(req); - } - }; - - var csrf = { - token: null, - - handle: function (success, data) { - this.token = data.token; - success(); - } - }; - - return function (url, settings) { - if (typeof url === 'object' && typeof settings === 'undefined') { - settings = url; - settings.method = settings.form.method; - url = settings.form.action; - } - if (url === '/csrf') { - settings.type = 'poll'; - settings.pollDelay = 1000 * 60 * 59; - settings.success = csrf.handle.bind(csrf, settings.success); - } - proxy.add(request.create(url, settings)); - }; -}()); diff --git a/uweb3/scaffold/base/static/scripts/uweb-dynamic.js b/uweb3/scaffold/base/static/scripts/uweb-dynamic.js deleted file mode 100644 index 38aeeb0f..00000000 --- a/uweb3/scaffold/base/static/scripts/uweb-dynamic.js +++ /dev/null @@ -1,176 +0,0 @@ -var ud = ud || {}; -var _paq = _paq || []; - - -class Page { - html = null - - constructor(page){ - this.page_hash = page[2].page_hash; - this.content_hash = page[2].content_hash; - this.template = page[2].template; - this.replacements = page[2].replacements; - } - -} - -(function () { - 'use strict'; - let i = 0; - let getRawTemplateRoute = "getrawtemplate"; - let cacheHandler = { - previous_key: null, - create: function(page){ - if(this.cacheSize() >= 5){ - this.delete(0); - } - window.localStorage.setItem(page.page_hash, - JSON.stringify({ - 'created': new Date().getTime(), - 'content_hash': page.content_hash, - 'replacements': page.replacements, - 'template': page.template, - 'html': page.html - })); - this.previous_key = page.page_hash; - return 200 - }, - insertHTML: function(html){ - if(this.previous_key){ - let storedPage = this.read(this.previous_key); - if(!storedPage.html){ - storedPage.html = html; - window.localStorage.setItem(this.previous_key, JSON.stringify(storedPage)); - } - return storedPage; - } - }, - read: function(page_hash){ - return JSON.parse(window.localStorage.getItem(page_hash)); - }, - delete: function(index){ - let key = window.localStorage.key(index); - let oldest_item = { - created: null, - key: null - } - if(index === 0){ - const items = { ...localStorage }; - for(let item in items){ - let current = this.read(item); - if(current.created < oldest_item.created || oldest_item.created == null){ - oldest_item.created = current.created; - oldest_item.key = item; - } - } - return window.localStorage.removeItem(oldest_item.key); - } - window.localStorage.removeItem(key); - }, - cacheSize: function(){ - return window.localStorage.length; - } - } - - function handleAnchors(){ - var anchors = document.getElementsByTagName('a'); - for(var i=0;i0){ - var data = {}; - fetchPage(path, data); - event.preventDefault(); - } - } - } - - function handleClick(event){ - if(event.target.tagName == 'A'){ - var path = localPart(event.target.href); - if(path.length>0){ - if(event.altKey){ - // TODO: delete this when done - path += `&variable=newContent${i}&variable2=moreContent${i}`; - }else{ - path += '&variable=test&variable2=moresamecontent'; - } - fetchPage(path); - event.preventDefault(); - } - } - } - - function localPart(url){ - if(url.startsWith(window.location.origin)){ - return url.substring(window.location.origin.length); - } - if(url.startsWith('//'+window.location.host)){ - return url.substring(window.location.host.length+2); - } - if(url.startsWith('/') && !url.startsWith('//')){ - return url; - } - return false; - } - - function fetchPage(url, data){ - ud.ajax(url, { success: handlePage }); - i++; - } - - function handlePage(data, url){ - // If the page is the same but the content is different we can retrieve the page from the hash and replace the placeholders with new values - // If the page is different we need to reload everything and update the cache - // Create a new instance of the page object. This only happens on the first call. - if(url.split('?').length >= 2){ - // console.log(url); - url = url.split('?')[1]; - } - if(typeof data === 'object'){ - const { content_hash, page_hash } = data[2]; - const cached = cacheHandler.read(page_hash); - if(cached){ - if(cached.content_hash === content_hash){ - // console.log(`Retrieving page from hash: ${page_hash} with content hash: ${content_hash}`); - let template = new Template(cached.html, cached.replacements); - document.querySelector('html').innerHTML = template.template; - }else{ - // console.log(`Retrieving page from hash: ${page_hash} with content hash: ${content_hash}`); - let template = new Template(cached.html, data[2].replacements); - document.querySelector('html').innerHTML = template.template; - } - }else{ - // If there is no cached page... - cacheHandler.create(new Page(data)); - // return ud.ajax(`/getrawtemplate?template=${data[2].template}&content_hash=${data[2].content_hash}`, {success: handlePage, mimeType: 'text/html'}); - return ud.ajax(`/${getRawTemplateRoute}?${url}&content_hash=${data[2].content_hash}`, {success: handlePage, mimeType: 'text/html'}); - - } - }else if(typeof data === 'string'){ - let html = cacheHandler.insertHTML(data); - let template = new Template(html.html, html.replacements); - document.querySelector('html').innerHTML = template.template; - } - handleAnchors(); - } - - function init(){ - handleAnchors(); - } - - init(); - -}()); diff --git a/uweb3/scaffold/base/static/scripts/uweb3-template-parser.js b/uweb3/scaffold/base/static/scripts/uweb3-template-parser.js deleted file mode 100644 index c5837844..00000000 --- a/uweb3/scaffold/base/static/scripts/uweb3-template-parser.js +++ /dev/null @@ -1,175 +0,0 @@ -class Template { - openScopes = []; - - get FUNCTION() { - return /\{\{\s*(.*?)\s*\}\}/mg; - } - - get TAG() { - return /(\[\w+(?:(?::[\w-]+)+)?(?:(?:\|[\w-]+(?:\([^()]*?\))?)+)?\])/gm; - } - - constructor(template, replacements){ - window.replacements = replacements; - this.inForLoop = false; - this.tmp = {}; - this.scopes = []; - this.AddString(template); - this.template = ""; - - for(let property in this.tmp){ - this.template += this.tmp[property].nodes; - } - for(let replacement in replacements){ - this.template = this.template.split(replacement).join(replacements[replacement]); - } - } - - AddString(template) { - let nodes = template.split(this.FUNCTION); - nodes.map((node, index) => { - let tmp_node = node.split(" "); - let func = tmp_node.shift(); - func = func.charAt(0).toUpperCase() + func.substring(1); - - if(index % 2){ - this._ExtendFunction(func, tmp_node, index); - }else{ - this._ExtendText(node, index) - } - }); - console.log(this.scopes); - console.log(this.tmp); - this._EvaluateScope(); - } - _EvaluateScope(){ - this.scopes.map((object, index) => { - console.log(object); - // let deleteScopes = false; - // for(let branch in object.branches){ - // if(!object.branches[branch].istrue){ - // //Delete all the scopes from which the condition was not met - // if(object.branches[branch].istrue !== undefined || deleteScopes){ - // //If the branch returns undefined its the last clause in the if statement so it will always be true - // delete this.tmp[object.branches[branch].index + 1]; - // } - // }else{ - // deleteScopes = true; - // } - // } - }); - } - _ExtendFunction(func, nodes, index) { - this[`_TemplateConstruct${func}`](nodes, index); - } - - _AddToOpenScope(item){ - // this.scopes[this.scopes.length - 1]; - } - - _StartScope(scope){ - if(this.openScopes.length > 0){ - this.openScopes[this.openScopes.length - 1].branches.push(scope); - }else{ - this.scopes.push(scope); - } - } - - _ExtendText(nodes, index){ - this.tmp[index] = { nodes: nodes }; - } - - _TemplateConstructIf(nodes, index){ - //Processing for {{ if }} template syntax - this._StartScope(new TemplateConditional(nodes.join(' '), index)); - } - - _TemplateConstructFor(nodes, index){ - let template = new TemplateLoop(nodes.join(' '), index); - if(this.openScopes.length > 0){ - this.openScopes.push(template); - }else{ - // this.scopes.push(template); - this.openScopes.push(template); - } - } - - _TemplateConstructEndfor(nodes, index){ - // this.openScopes[this.openScopes.length - 1].branches.push({index: index}); - this.scopes.push(this.openScopes[this.openScopes.length - 1]); - this.openScopes.pop(); - } - - _TemplateConstructElif(nodes, index){ - this.scopes[this.scopes.length - 1] = this.scopes[this.scopes.length - 1].Elif(index, nodes.join(' ')) - } - _TemplateConstructElse(nodes, index){ - //Processing for {{ else }} template syntax. - this.scopes[this.scopes.length - 1] = this.scopes[this.scopes.length - 1].Else(index) - } - - _TemplateConstructEndif(){ - //Processing for {{ endif }} template syntax. - // self._CloseScope(TemplateConditional) - } -} - -class TemplateConditional { - get TAG() { - return /(\[\w+(?:(?::[\w-]+)+)?(?:(?:\|[\w-]+(?:\([^()]*?\))?)+)?\])/gm; - } - - constructor(expr, index) { - this.branches = []; - this.NewBranch(expr, index); - } - - NewBranch(expr, index){ - let isTrue = this._EvaluateClause(expr); - this.branches.push({ index: index, expr: expr }); - } - - _EvaluateClause(expr){ - expr = expr.replace(" and ", " && "); - expr = expr.replace(" or ", " || "); - let temp_expr = "" - let variables = "" - if(expr.search(' in ') !== -1){ - throw "NotImplemented" - }else{ - expr.match(this.TAG).map((value) => { - //if the regex is a match it is a variable else its a variable function such as [variable|len] - let regex = new RegExp(/(\[\w+?\])/gm); - if(regex.test(value)){ - variables += `let ${value.substring(1, value.length - 1)} = "${window.replacements[value]}";`; - temp_expr = expr.split(value).join(value.substring(1, value.length - 1)); - }else{ - temp_expr = expr.split(value).join(window.replacements[value]); - } - }); - } - return Function(`${variables} if(${temp_expr}){return true}else{return false}`)() - } - - Elif(index, expr){ - let isTrue = this._EvaluateClause(expr); - this.branches.push({ index: index, expr: expr, istrue: isTrue }); - return this; - } - - Else(index){ - this.branches.push({ index: index }); - return this; - } -} - -class TemplateLoop { - constructor(expr, index) { - this.branches = []; - this.NewBranch(expr, index); - } - - NewBranch(expr, index){ - this.branches.push({ index: index, expr: expr}); - } -} \ No newline at end of file diff --git a/uweb3/scaffold/base/templates/403.html b/uweb3/scaffold/base/templates/403.html deleted file mode 100644 index 2596fa68..00000000 --- a/uweb3/scaffold/base/templates/403.html +++ /dev/null @@ -1 +0,0 @@ -403 [error] \ No newline at end of file diff --git a/uweb3/scaffold/base/templates/404.html b/uweb3/scaffold/base/templates/404.html deleted file mode 100644 index d2259e5f..00000000 --- a/uweb3/scaffold/base/templates/404.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - uWeb3 project scaffold - - -

This is not the page you're looking for (HTTP 404)

-

- The URL you requested ("[path]") doesn't exist. -

- - diff --git a/uweb3/scaffold/base/templates/footer.html b/uweb3/scaffold/base/templates/footer.html deleted file mode 100644 index e69de29b..00000000 diff --git a/uweb3/scaffold/base/templates/header.html b/uweb3/scaffold/base/templates/header.html deleted file mode 100644 index e69de29b..00000000 diff --git a/uweb3/scaffold/base/templates/index.html b/uweb3/scaffold/base/templates/index.html deleted file mode 100644 index d135bbbf..00000000 --- a/uweb3/scaffold/base/templates/index.html +++ /dev/null @@ -1,35 +0,0 @@ - - - - - Header - Underdark CSS - - Uweb - - - - - - - - -
-
- - - -
-
- - \ No newline at end of file diff --git a/uweb3/scaffold/base/templates/sqlalchemy.html b/uweb3/scaffold/base/templates/sqlalchemy.html deleted file mode 100644 index 2181d0f1..00000000 --- a/uweb3/scaffold/base/templates/sqlalchemy.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - Header - Underdark CSS - - Uweb - - - - - - - - -
-
- - - -
-
-
-

Sqlalchemy

- Output is in the console. -
- - \ No newline at end of file diff --git a/uweb3/scaffold/base/templates/test.html b/uweb3/scaffold/base/templates/test.html deleted file mode 100644 index a2834292..00000000 --- a/uweb3/scaffold/base/templates/test.html +++ /dev/null @@ -1,54 +0,0 @@ - - - - - Header - Underdark CSS - - Uweb - - - - - - - - -
-
- - -
-
-
-
-

Alt + click to generate new content

- -
- - {{if [variable]}} - {{if [variable]}} - [variable] - {{endif}} - {{if [variable|len] > '100'}} - [variable] - {{else}} - oi - {{endif}} - {{endif}} -

- {{ for item in [variable] }} [item] - {{endfor}} -
- - - - - - - \ No newline at end of file diff --git a/uweb3/scaffold/base/templates/test2.html b/uweb3/scaffold/base/templates/test2.html deleted file mode 100644 index db537e67..00000000 --- a/uweb3/scaffold/base/templates/test2.html +++ /dev/null @@ -1,51 +0,0 @@ - - - - - Header - Underdark CSS - - Uweb - - - - - - - - -
-
- - -
-
-
-
-

Alt + click to generate new content

- -
- - {{ for item in [variable] }} - [item] - {{if [variable]}}test{{endif}} - {{for item in [variable2] }} - {{if [variable]}}test{{endif}} - test - {{endfor}} - {{endfor}} - - -
- - - - - - - diff --git a/uweb3/scaffold/routes/__init__.py b/uweb3/scaffold/routes/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/uweb3/scaffold/routes/socket_handler.py b/uweb3/scaffold/routes/socket_handler.py deleted file mode 100644 index 43820c5c..00000000 --- a/uweb3/scaffold/routes/socket_handler.py +++ /dev/null @@ -1,15 +0,0 @@ -#!/usr/bin/python3 -"""Request handlers for the uWeb3 project scaffold""" - -import uweb3 -from uweb3 import PageMaker - -class SocketHandler(PageMaker): - """Holds all the request handlers for the application""" - - def EventHandler(sid, msg): - # print(sid, msg) - print("hello world from sockethandler") - - def Connect(sid, env): - print(sid, env) \ No newline at end of file diff --git a/uweb3/scaffold/routes/sqlalchemy.py b/uweb3/scaffold/routes/sqlalchemy.py deleted file mode 100644 index 6aac9729..00000000 --- a/uweb3/scaffold/routes/sqlalchemy.py +++ /dev/null @@ -1,105 +0,0 @@ -#!/usr/bin/python3 -"""Request handlers for the uWeb3 project scaffold""" - -from uweb3 import SqAlchemyPageMaker -from uweb3.alchemy_model import AlchemyRecord - -from sqlalchemy import Column, Integer, String, update, MetaData, Table, ForeignKey, inspect -from sqlalchemy.ext.declarative import declarative_base -from sqlalchemy.orm import sessionmaker, relationship, lazyload - -Base = declarative_base() - -class User(AlchemyRecord, Base): - __tablename__ = 'alchemy_users' - - id = Column(Integer, primary_key=True) - username = Column(String, nullable=False, unique=True) - password = Column(String, nullable=False) - authorid = Column('authorid', Integer, ForeignKey('author.id')) - children = relationship("Author", lazy="select") - - - def __init__(self, *args, **kwargs): - super(User, self).__init__(*args, **kwargs) - -class Author(AlchemyRecord, Base): - __tablename__ = 'author' - - id = Column(Integer, primary_key=True) - name = Column(String, unique=True) - personid = Column('personid', Integer, ForeignKey('persons.id')) - children = relationship("Persons", lazy="select") - - -class Persons(AlchemyRecord, Base): - __tablename__ = 'persons' - - id = Column(Integer) - name = Column(String, primary_key=True) - - -def buildTables(connection, session): - meta = MetaData() - Table( - 'alchemy_users', meta, - Column('id', Integer, primary_key=True), - Column('username', String(255), nullable=False, unique=True), - Column('password', String(255), nullable=False), - Column('authorid', Integer, ForeignKey('author.id')), - ) - Table( - 'author', meta, - Column('id', Integer, primary_key=True), - Column('name', String(32), nullable=False), - Column('personid', Integer, ForeignKey('persons.id')) - ) - Table( - 'persons', meta, - Column('id', Integer,primary_key=True), - Column('name', String(32), nullable=False) - ) - - meta.create_all(connection) - - Persons.Create(session, {'name': 'Person name'}) - Author.Create(session, {'name': 'Author name', 'personid': 1}) - Author.Create(session, {'name': 'Author number 2', 'personid': 1}) - User.Create(session, {'username': 'name', 'password': 'test', 'authorid': 1}) - - -class UserPageMaker(SqAlchemyPageMaker): - """Holds all the request handlers for the application""" - - def Sqlalchemy(self): - """Returns the index template""" - tables = inspect(self.engine).get_table_names() - if not 'alchemy_users' in tables or not 'author' in tables or not 'persons' in tables: - buildTables(self.engine, self.session) - - user = User.FromPrimary(self.session, 1) - # print(User.Create(self.session, {'username': 'hello', 'password': 'test', 'authorid': 1})) - # print("Returns user with primary key 1: ", user) - # print("Will only load the children when we ask for them: ", user.children) - # print("Conditional list, lists users with id < 10: ", list(User.List(self.session, conditions=[User.id <= 10]))) - print("List item 0: ", list(User.List(self.session, conditions=[User.id <= 10]))[0]) - # print("List item 0.children: ", list(User.List(self.session, conditions=[User.id <= 10]))[0].children) - - # User.Update(self.session, [User.id > 2, User.id < 100], {User.username: 'username', User.password: 'password'}) - # print("User from primary key", user) - # user.Delete() - # print(user.children) - # print("deleted", User.DeletePrimary(self.session, user.key)) - # print(User.List(self.session, conditions=[User.id >= 1, User.id <= 10])) - # print(user) - # print("FromPrimary: ", user) - # print(self.session.query(Persons, Author).join(Author).filter().all()) - # user.username = f'USERNAME{result.id}' - # user.Save() - # user.author.name = f'AUTHOR{result.id}' - # user.author.Save() - # print("EditedUser", user) - # user_list = list(User.List(self.session, order=(User.id.desc(), User.username.asc()))) - # print("DeletePrimary: ", User.DeletePrimary(self.session, result.id)) - # print('---------------------------------------------------------------------------') - return self.parser.Parse('sqlalchemy.html') diff --git a/uweb3/scaffold/routes/test.py b/uweb3/scaffold/routes/test.py deleted file mode 100644 index 264b47bb..00000000 --- a/uweb3/scaffold/routes/test.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/python3 -"""Request handlers for the uWeb3 project scaffold""" - -import uweb3 -import json -from uweb3 import PageMaker -from uweb3.ext_lib.libs.safestring import SQLSAFE, HTMLsafestring -from uweb3.model import SettingsManager - -class Test(PageMaker): - """Holds all the request handlers for the application""" - - @staticmethod - def Limit(length=80): - """Returns a closure that limits input to a number of chars/elements.""" - return lambda string: string[:length] - - def Test(self): - """Returns the index template""" - self.parser.RegisterFunction('substr', self.Limit) - return self.parser.Parse('test.html', variable='test') - - def GetRawTemplate(self): - """Endpoint that only returns the raw template""" - template = self.get.getfirst('template') - content_hash = self.get.getfirst('content_hash') - if not template or not content_hash: - return 404 - del self.get['template'] - del self.get['content_hash'] - kwds = {} - for item in self.get: - kwds[item] = self.get.getfirst(item) - content = self.parser.Parse(template, returnRawTemplate=True, **kwds) - if content.content_hash == content_hash: - return content - return 404 - - def Parsed(self): - self.parser.RegisterFunction('substr', self.Limit) - kwds = {} - template = self.get.getfirst('template') - del self.get['template'] - for item in self.get: - kwds[item] = self.get.getfirst(item) - try: - self.parser.noparse = True - content = self.parser.Parse( - template, **kwds) - finally: - self.parser.noparse = False - return json.dumps(((self.req.headers.get('http_x_requested_with', None), self.parser.noparse, content))) - - def StringEscaping(self): - if self.post: - result = SQLSAFE(self.post.getfirst('sql'), values=(self.post.getfirst('value1'), self.post.getfirst('value2')), unsafe=True) - print(result) - print(result.unescape(result)) - - return self.req.Redirect('/test') \ No newline at end of file diff --git a/uweb3/scaffold/serve.py b/uweb3/scaffold/serve.py deleted file mode 100644 index 46616907..00000000 --- a/uweb3/scaffold/serve.py +++ /dev/null @@ -1,31 +0,0 @@ -"""Starts a simple application development server.""" - -# Application -import base -import socketio -from uweb3.sockets import Uweb3SocketIO - -def websocket_routes(sio): - @sio.on("connect") - def test(sid, env): - print("WEBSOCKET ROUTE CALLED: ", sid, env) - -def main(): - sio = socketio.Server() - # websocket_routes(sio) - return sio - -if __name__ == '__main__': - sio = main() - Uweb3SocketIO(base.main(sio), sio) - - -# # Application -# import base - -# def main(): -# app = base.main() -# app.serve() - -# if __name__ == '__main__': -# main() \ No newline at end of file From 99babbbb34658da36e45284218ee7c00cc2a3327 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Thu, 14 May 2020 11:11:32 +0200 Subject: [PATCH 017/118] Removed all login/session support from uWeb3 --- uweb3/pagemaker/login.py | 70 --------------------- uweb3/pagemaker/session.py | 124 ------------------------------------- uweb3/response.py | 14 +++++ 3 files changed, 14 insertions(+), 194 deletions(-) delete mode 100644 uweb3/pagemaker/login.py delete mode 100644 uweb3/pagemaker/session.py diff --git a/uweb3/pagemaker/login.py b/uweb3/pagemaker/login.py deleted file mode 100644 index 249f8216..00000000 --- a/uweb3/pagemaker/login.py +++ /dev/null @@ -1,70 +0,0 @@ -#!/usr/bin/python -"""uWeb3 PageMaker Mixins for login/authentication purposes. - -Contains both the Underdark Login Framework and OpenID implementations -""" - -# Standard modules -import binascii -import hashlib -import os -import base64 - -# Package modules -from uweb3.model import SecureCookie -from .. import model - -class User(model.Record): - """Abstraction for the `user` table.""" - SALT_BYTES = 8 - - @classmethod - def FromName(cls, connection, username): - """Returns a User object based on the given username.""" - with connection as cursor: - safe_name = connection.EscapeValues(username) - user = cursor.Select( - table=cls.TableName(), - conditions='name=%s' % safe_name) - if not user: - raise cls.NotExistError('No user with name %r' % username) - return cls(connection, user[0]) - - @classmethod - def HashPassword(cls, password, salt=None): - if not salt: - salt = cls.SaltBytes() - if (len(salt) * 3) / 4 - salt.decode('utf-8').count('=', -2) != cls.SALT_BYTES: - raise ValueError('Salt is of incorrect length. Expected %d, got: %d' % ( - cls.SALT_BYTES, len(salt))) - m = hashlib.sha256() - m.update(password.encode("utf-8") + binascii.hexlify(salt)) - password = m.hexdigest() - return { 'password': password, 'salt': salt } - - @classmethod - def SaltBytes(cls): - """Returns the configured number of random bytes for the salt.""" - random_bytes = os.urandom(cls.SALT_BYTES) - return base64.b64encode(random_bytes).decode('utf-8').encode('utf-8') #we do this to cast this byte to utf-8 - - def UpdatePassword(self, plaintext): - """Stores a new password hash and salt, from the given plaintext.""" - self.update(self.HashPassword(plaintext)) - self.Save() - - def VerifyChallenge(self, attempt, challenge): - """Verifies the password hash against the stored hash. - - Both the password hash (attempt) and the challenge should be provided - as raw bytes. - """ - password = binascii.hexlify(self['password']) - actual_pass = hashlib.sha256(password + binascii.hexlify(challenge)).digest() - return attempt == actual_pass - - def VerifyPlaintext(self, plaintext): - """Verifies a given plaintext password.""" - salted = self.HashPassword(plaintext, self['salt'].encode('utf-8'))['password'] - return salted == self['password'] - diff --git a/uweb3/pagemaker/session.py b/uweb3/pagemaker/session.py deleted file mode 100644 index 679c6c74..00000000 --- a/uweb3/pagemaker/session.py +++ /dev/null @@ -1,124 +0,0 @@ -#!/usr/bin/python -"""uWeb3 PageMaker Mixins for session management purposes.""" - -# Standard modules -import binascii -import os -import datetime -import pytz - -# Package modules -from .. import model - -# ############################################################################## -# Record classes for session management -# -# Model class have many methods. -# pylint:disable=R0904 - -class Session(model.Record): - """Abstraction for the `session` table""" - - _PRIMARY_KEY = 'session' -# pylint:enable=R0904 - -# ############################################################################## -# Pagemaker Mixin class for session management -# -class SessionMixin(object): - """Provides session management for uWeb3""" - - class NoSessionError(Exception): - """Custom exception for user not having a (unexpired) session cookie.""" - - class SecurityError(Exception): - """Custom exception raised for not passing security constraints set on the - session.""" - - class XsrfError(Exception): - """Custom exception raised in case of a detected XSRF attack.""" - - SESSION_TABLE = Session - - def _ULF_DeleteSession(self, cookie_name): - """Destroys a user session with `cookie_name`. Used for logging out.""" - try: - sessionid = self.cookies[cookie_name] - except KeyError: - raise self.NoSessionError( - 'User does not have a session cookie with name %s' % cookie_name) - try: - binsessid = binascii.unhexlify(sessionid) - self.SESSION_TABLE.DeletePrimary(self.connection, binsessid) - # Set a junk cookie that expires in 1 second. - self.req.AddCookie(cookie_name, 'deleted', path='/', max_age=1, - httponly=True) - except model.NotExistError: - raise self.NoSessionError( - 'There is no session associated with ID %s' % sessionid) - - def _ULF_CheckXsrf(self, cookie_name, field_name="xsrf"): - """Checks if the cookie named `cookie_name` matches the field `field_name` - - Used to check if an XSRF is happening. Returns `True` if an XSRF is - detected. - """ - try: - sessionid = self.cookies[cookie_name] - except KeyError: - raise self.NoSessionError( - 'User does not have a session cookie with name %s' % cookie_name) - if self.req.env['REQUEST_METHOD'] == "POST": - if sessionid != self.post.getfirst(field_name): - raise self.XsrfError("An XSRF attack was detected for this request.") - else: - if sessionid != self.get.getfirst(field_name): - raise self.XsrfError("An XSRF attack was detected for this request.") - - def _ULF_GetSessionId(self, cookie_name): - try: - sessionid = self.cookies[cookie_name] - except KeyError: - raise self.NoSessionError( - 'User does not have a session cookie with name %s' % cookie_name) - return sessionid - - def _ULF_GetSession(self, cookie_name): - """Fetches a user ID associated with a session ID set on `cookie_name`.""" - remote = self.req.env['REMOTE_ADDR'] # Get remote IP of user. - try: - sessionid = self.cookies[cookie_name] - except KeyError: - raise self.NoSessionError( - 'User does not have a session cookie with name %s' % cookie_name) - try: - binsessid = binascii.unhexlify(sessionid) - session = Session.FromPrimary(self.connection, binsessid) - remote = self.req.env['REMOTE_ADDR'] - if (session['expiry'] < datetime.datetime.now(pytz.UTC)): - raise self.NoSessionError('The user session has expired.') - if (session['iplocked'] and remote != session['remote']): - raise self.SecurityError('This session is locked to another IP.') - user = session['user'] - except model.NotExistError: - raise self.NoSessionError( - 'There is no session associated with ID %s' % sessionid) - return user - - def _ULF_SetSession(self, cookie_name, uid, expiry=86400, locktoip=True): - """Sets a user ID to `uid` on cookie `cookie_name`, gives a new cookie to - the user with this cookie name. - - Takes an optional `expiry` argument which defaults to 86400 seconds. - Also takes an optional `locktoip` argument which defaults to True -- - which causes the session to be locked to the user's IP""" - random_id = os.urandom(16) - # The random ID needs to be converted to hex for the cookie. - self.req.AddCookie(cookie_name, binascii.hexlify(random_id), path='/', - max_age=expiry, httponly=True) - now = datetime.datetime.utcnow() - expirationdate = now + datetime.timedelta(seconds=expiry) - self.SESSION_TABLE.Create(self.connection, { - 'session': random_id, 'user': uid, - 'remote': self.req.env['REMOTE_ADDR'], 'expiry': '2021-02-18 11:15:45', - 'iplocked': int(locktoip)}) diff --git a/uweb3/response.py b/uweb3/response.py index c0640e7f..63cf1d23 100644 --- a/uweb3/response.py +++ b/uweb3/response.py @@ -88,3 +88,17 @@ def __repr__(self): def __str__(self): return self.content + +class Redirect(Response): + """A response tailored to do redirects.""" + REDIRECT_PAGE = ('Page moved' + 'Page moved, please follow this link' + '') + #TODO make sure we inject cookies set on the previous response by copying any Set-Cookie headers from them into these headers. + def __init__(self, location, httpcode=307): + super(Redirect, self).__init__( + content=self.REDIRECT_PAGE % location, + content_type='text/html', + httpcode=httpcode, + headers={'Location': location}) + From db85b538e59d2933666abb3e581d40ed71160201 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Mon, 18 May 2020 11:20:22 +0200 Subject: [PATCH 018/118] uWeb3 now places incoming PUT/DELETE variables in the correct self.vars --- uweb3/pagemaker/new_decorators.py | 16 ++++++++-------- uweb3/request.py | 13 ++++++++++--- 2 files changed, 18 insertions(+), 11 deletions(-) diff --git a/uweb3/pagemaker/new_decorators.py b/uweb3/pagemaker/new_decorators.py index b00d36ed..a0a4841f 100644 --- a/uweb3/pagemaker/new_decorators.py +++ b/uweb3/pagemaker/new_decorators.py @@ -7,8 +7,8 @@ class XSRF(object): # secret = str(os.urandom(64)) secret = "test" def __init__(self, AddCookie, post): - """Checks if cookie with xsrf key is present. - + """Checks if cookie with xsrf key is present. + If not generates xsrf token and places it in a cookie. Checks if xsrf token in post is equal to the one in the cookie and returns True when they do not match and False when they do match for the 'incorrect_xsrf_token' flag. @@ -16,16 +16,16 @@ def __init__(self, AddCookie, post): self.unix_timestamp = time.mktime(datetime.datetime.now().date().timetuple()) self.AddCookie = AddCookie self.post = post - + def is_valid_xsrf_token(self, userid): """Validate given xsrf token based on userid - + Arguments: @ userid: str/int - + Returns: IsValid: boolean - """ + """ token = self.Generate_xsrf_token(userid) if not self.post.get('xsrf'): return False @@ -33,7 +33,7 @@ def is_valid_xsrf_token(self, userid): return False return True - def Generate_xsrf_token(self, userid): + def Generate_xsrf_token(self, userid): hashed = (str(self.unix_timestamp) + self.secret + userid).encode('utf-8') h = hashlib.new('ripemd160') h.update(hashed) @@ -43,7 +43,7 @@ def loggedin(f): """Decorator that checks if the user requesting the page is logged in based on set cookie.""" def wrapper(*args, **kwargs): if not args[0].user: - return args[0].req.Redirect('/login', http_code=303) + return args[0].req.Redirect('/login', httpcode=303) return f(*args, **kwargs) return wrapper diff --git a/uweb3/request.py b/uweb3/request.py index 5fa94cfe..48e57bd4 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -93,7 +93,10 @@ def __init__(self, env, registry): self.vars = {'cookie': dict((name, value.value) for name, value in Cookie(self.env.get('HTTP_COOKIE')).items()), 'get': PostDictionary(cgi.parse_qs(self.env.get('QUERY_STRING'))), - 'post': PostDictionary()} + 'post': PostDictionary(), + 'put': PostDictionary(), + 'delete': PostDictionary(), + } self.env['host'] = self.headers.get('Host', '') if self.method == 'POST': @@ -110,6 +113,10 @@ def __init__(self, env, registry): self.vars['post'] = PostDictionary(form) for f in files: self.vars['post'][f] = files.get(f) + else: + if self.method in ('PUT', 'DELETE'): + stream, form, files = parse_form_data(self.env) + self.vars[self.method.lower()] = PostDictionary(form) @property def path(self): @@ -121,7 +128,7 @@ def response(self): self._response = response.Response() return self._response - def Redirect(self, location, http_code=307): + def Redirect(self, location, httpcode=307): REDIRECT_PAGE = ('Page moved' 'Page moved, please follow this link' '').format(location) @@ -132,7 +139,7 @@ def Redirect(self, location, http_code=307): return response.Response( content=REDIRECT_PAGE, content_type=self.response.headers.get('Content-Type', 'text/html'), - httpcode=http_code, + httpcode=httpcode, headers=headers ) From 5885ca9578fc9f80ad42d832ae092fcf64027deb Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Mon, 18 May 2020 11:46:29 +0200 Subject: [PATCH 019/118] Updated the checkxsrf decorator, now removes form data in PageMaker and the Request class on incorrect XSRF. Also sets a invalid_form_data attribute only accessible in the request it failed on --- uweb3/pagemaker/__init__.py | 2 + uweb3/pagemaker/new_decorators.py | 95 ++++++++++++++++++------------- 2 files changed, 59 insertions(+), 38 deletions(-) diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 95091524..07a10b37 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -170,6 +170,8 @@ def __init__(self, req, config=None, secure_cookie_secret=None, executing_path=N self.cookies = req.vars['cookie'] self.get = req.vars['get'] self.post = req.vars['post'] + self.put = req.vars['put'] + self.delete = req.vars['delete'] self.options = config or {} self.persistent = self.PERSISTENT self.secure_cookie_connection = (self.req, self.cookies, secure_cookie_secret) diff --git a/uweb3/pagemaker/new_decorators.py b/uweb3/pagemaker/new_decorators.py index a0a4841f..569a91d3 100644 --- a/uweb3/pagemaker/new_decorators.py +++ b/uweb3/pagemaker/new_decorators.py @@ -2,42 +2,7 @@ import time import datetime import hashlib - -class XSRF(object): - # secret = str(os.urandom(64)) - secret = "test" - def __init__(self, AddCookie, post): - """Checks if cookie with xsrf key is present. - - If not generates xsrf token and places it in a cookie. - Checks if xsrf token in post is equal to the one in the cookie and returns - True when they do not match and False when they do match for the 'incorrect_xsrf_token' flag. - """ - self.unix_timestamp = time.mktime(datetime.datetime.now().date().timetuple()) - self.AddCookie = AddCookie - self.post = post - - def is_valid_xsrf_token(self, userid): - """Validate given xsrf token based on userid - - Arguments: - @ userid: str/int - - Returns: - IsValid: boolean - """ - token = self.Generate_xsrf_token(userid) - if not self.post.get('xsrf'): - return False - if self.post.get('xsrf') != token: - return False - return True - - def Generate_xsrf_token(self, userid): - hashed = (str(self.unix_timestamp) + self.secret + userid).encode('utf-8') - h = hashlib.new('ripemd160') - h.update(hashed) - return h.hexdigest() +from uweb3.request import PostDictionary def loggedin(f): """Decorator that checks if the user requesting the page is logged in based on set cookie.""" @@ -47,6 +12,16 @@ def wrapper(*args, **kwargs): return f(*args, **kwargs) return wrapper +def clear_form_data(*args): + method = args[0].req.method.lower() + #Set an attribute in the pagemaker that holds the form data on an invalid XSRF validation + args[0].invalid_form_data = getattr(args[0], method) + #Remove the form data from the PageMaker + setattr(args[0], method, PostDictionary()) + #Remove the form data from the Request class + args[0].req.vars[method] = PostDictionary() + return args + def checkxsrf(f): """Decorator that checks the user's XSRF. @@ -54,6 +29,10 @@ def checkxsrf(f): (post) request. Make sure to have xsrf_enabled = True in the config.ini """ def wrapper(*args, **kwargs): + #TODO: How do we supply the seed for generating an XSRF token? + #In this case we use the user_id but how do we get it + args[0].user = {'user_id': '1'} + xsrf_cookie = args[0].cookies.get('xsrf') xsrf = XSRF(args[0].req.AddCookie, args[0].post) if args[0].req.method == "GET": @@ -71,13 +50,53 @@ def wrapper(*args, **kwargs): else: #On a post request check if there is a cookie with xsrf and if the post contains an xsrf input if not xsrf_cookie: + args = clear_form_data(*args) return args[0].XSRFInvalidToken('XSRF cookie is missing') if not args[0].post.get('xsrf'): - args[0].post = {} + args = clear_form_data(*args) return args[0].XSRFInvalidToken('XSRF token is missing') #Validate token if not xsrf.is_valid_xsrf_token(args[0].user.get('user_id')): + args = clear_form_data(*args) return args[0].XSRFInvalidToken('XSRF token is not valid') args[0].xsrf = xsrf_cookie return f(*args, **kwargs) - return wrapper \ No newline at end of file + return wrapper + + +class XSRF(object): + # secret = str(os.urandom(64)) + secret = "test" + def __init__(self, AddCookie, post): + """Checks if cookie with xsrf key is present. + + If not generates xsrf token and places it in a cookie. + Checks if xsrf token in post is equal to the one in the cookie and returns + True when they do not match and False when they do match for the 'incorrect_xsrf_token' flag. + """ + self.unix_timestamp = time.mktime(datetime.datetime.now().date().timetuple()) + self.AddCookie = AddCookie + self.post = post + + def is_valid_xsrf_token(self, userid): + """Validate given xsrf token based on userid + + Arguments: + @ userid: str/int + + Returns: + IsValid: boolean + """ + token = self.Generate_xsrf_token(userid) + if not self.post.get('xsrf'): + return False + if self.post.get('xsrf') != token: + return False + return True + + def Generate_xsrf_token(self, userid): + hashed = (str(self.unix_timestamp) + self.secret + userid).encode('utf-8') + h = hashlib.new('ripemd160') + h.update(hashed) + return h.hexdigest() + From df4b0faaef14dd062db0a876d3768f8d34b7774f Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Tue, 19 May 2020 11:10:34 +0200 Subject: [PATCH 020/118] Updated the XSRF decorator --- uweb3/__init__.py | 33 ++++++++++++-- uweb3/helpers.py | 1 - uweb3/pagemaker/__init__.py | 76 ++++++++++++++++++++++++++----- uweb3/pagemaker/new_decorators.py | 70 ++-------------------------- uweb3/request.py | 1 - 5 files changed, 97 insertions(+), 84 deletions(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 662bae5d..a6e63098 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -34,6 +34,16 @@ from .helpers import StaticMiddleware from uweb3.model import SettingsManager + +def return_real_remote_addr(env): + """Returns the client addres, + if there is a proxy involved it will take the last IP addres from the HTTP_X_FORWARDED_FOR list + """ + try: + return env['HTTP_X_FORWARDED_FOR'].split(',')[-1].strip() + except KeyError: + return env['REMOTE_ADDR'] + class Error(Exception): """Superclass used for inheritance and external exception handling.""" @@ -171,8 +181,10 @@ def __init__(self, page_class, routes, executing_path=None): self.registry = Registry() self.registry.logger = logging.getLogger('root') self.router = Router(page_class).router(routes) - self.secure_cookie_secret = str(os.urandom(32)) self.setup_routing() + #generating random seeds on uWeb3 startup + self.secure_cookie_secret = str(os.urandom(32)) + self.XSRF_seed = str(os.urandom(32)) def __call__(self, env, start_response): """WSGI request handler. @@ -180,22 +192,35 @@ def __call__(self, env, start_response): response and returns a response iterator. """ req = request.Request(env, self.registry) + req.env['REAL_REMOTE_ADDR'] = return_real_remote_addr(req.env) try: method, args, hostargs, pagemaker = self.router(req.path, req.env['REQUEST_METHOD'], req.env['host'] ) - pagemaker = pagemaker(req, config=self.config.options, secure_cookie_secret=self.secure_cookie_secret, executing_path=self.executing_path) + pagemaker = pagemaker(req, + config=self.config.options, + secure_cookie_secret=self.secure_cookie_secret, + executing_path=self.executing_path, + XSRF_seed=self.XSRF_seed) response = self.get_response(pagemaker, method, args) except NoRouteError: #When we catch this error this means there is no method for the expected function #If this happens we default to the standard pagemaker because we don't know what the target pagemaker should be. #Then we set an internalservererror and move on - pagemaker = self.page_class(req, config=self.config.options, secure_cookie_secret=self.secure_cookie_secret, executing_path=self.executing_path) + pagemaker = self.page_class(req, + config=self.config.options, + secure_cookie_secret=self.secure_cookie_secret, + executing_path=self.executing_path, + XSRF_seed=self.XSRF_seed) response = pagemaker.InternalServerError(*sys.exc_info()) except Exception: #This should only happend when something is very wrong - pagemaker = PageMaker(req, config=self.config.options, secure_cookie_secret=self.secure_cookie_secret, executing_path=self.executing_path) + pagemaker = PageMaker(req, + config=self.config.options, + secure_cookie_secret=self.secure_cookie_secret, + executing_path=self.executing_path, + XSRF_seed=self.XSRF_seed) response = pagemaker.InternalServerError(*sys.exc_info()) if not isinstance(response, Response): diff --git a/uweb3/helpers.py b/uweb3/helpers.py index c33fb618..a744de8b 100644 --- a/uweb3/helpers.py +++ b/uweb3/helpers.py @@ -135,4 +135,3 @@ def handle(self, env, start_response, filename): return res else: return http404(env, start_response) - diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 07a10b37..738b306e 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -9,6 +9,7 @@ import sys import threading import time +import hashlib from base64 import b64encode from pymysql import Error as pymysqlerr @@ -19,7 +20,6 @@ RFC_1123_DATE = '%a, %d %b %Y %T GMT' - class ReloadModules(Exception): """Signals the handler that it should reload the pageclass""" @@ -141,6 +141,28 @@ def update(self, data=None, **kwargs): if kwargs: self.update(kwargs) +class XSRF(object): + def __init__(self, seed, remote_addr): + self.seed = seed + self.remote_addr = remote_addr + self.unix_today = time.mktime(datetime.datetime.now().date().timetuple()) + + def generate_token(self): + """Generate an XSRF token + + XSRF token is generated based on the unix timestamp from today, + a randomly generated seed and the IP addres from the user + """ + hashed = (str(self.unix_today) + self.seed + self.remote_addr).encode('utf-8') + h = hashlib.new('ripemd160') + h.update(hashed) + return h.hexdigest() + + def is_valid(self, supplied_token): + token = self.generate_token() + return token != supplied_token + + class BasePageMaker(object): """Provides the base pagemaker methods for all the html generators.""" # Constant for persistent storage accross requests. This will be accessible @@ -155,7 +177,12 @@ class BasePageMaker(object): # Default Static() handler cache durations, per MIMEtype, in days CACHE_DURATION = MimeTypeDict({'text': 7, 'image': 30, 'application': 7}) - def __init__(self, req, config=None, secure_cookie_secret=None, executing_path=None): + def __init__(self, + req, + config=None, + secure_cookie_secret=None, + executing_path=None, + XSRF_seed=None): """sets up the template parser and database connections Arguments: @@ -175,6 +202,35 @@ def __init__(self, req, config=None, secure_cookie_secret=None, executing_path=N self.options = config or {} self.persistent = self.PERSISTENT self.secure_cookie_connection = (self.req, self.cookies, secure_cookie_secret) + self.set_invalid_xsrf_token_flag(XSRF_seed) + + def set_invalid_xsrf_token_flag(self, XSRF_seed): + """Sets the invalid_xsrf_token flag to true or false""" + self.invalid_xsrf_token = False + if self.req.method != 'GET': + user_supplied_xsrf_token = getattr(self, self.req.method.lower()).get('xsrf') + xsrf = XSRF(XSRF_seed, self.req.env['REAL_REMOTE_ADDR']) + self.invalid_xsrf_token = xsrf.is_valid(user_supplied_xsrf_token) + #First we try to validate the token, then we check if the user has an xsrf cookie + self._Set_XSRF_cookie(XSRF_seed) + + + def _Set_XSRF_cookie(self, XSRF_seed): + """Checks if XSRF is enabled in the config and handles accordingly + + If XSRF is enabled it will check if there is an XSRF cookie, if not create one. + If XSRF is disabled nothing will happen + """ + if self.options.get('development'): + xsrf_enabled = self.options['development'].get('xsrf') + if xsrf_enabled == "True": + xsrf_cookie = self.cookies.get('xsrf') + if self.invalid_xsrf_token: + self.req.AddCookie("xsrf", XSRF(XSRF_seed, self.req.env['REAL_REMOTE_ADDR']).generate_token()) + return + if not xsrf_cookie: + self.req.AddCookie("xsrf", XSRF(XSRF_seed, self.req.env['REAL_REMOTE_ADDR']).generate_token()) + return def _PostRequest(self, response): if response.status == '500 Internal Server Error': @@ -186,18 +242,17 @@ def _PostRequest(self, response): cursor.Execute("ROLLBACK") except Exception: if hasattr(self, 'connection'): - if self.connection.open: - self.connection.close() - self.persistent.Del("__mysql") + if self.connection.open: + self.connection.close() + self.persistent.Del("__mysql") self.connection_error = False return response def XSRFInvalidToken(self, command): """Returns an error message regarding an incorrect XSRF token.""" - page_data = self.parser.Parse('403.html', error=command, - **self.CommonBlocks('Invalid XSRF token')) - return uweb3.Response(content=page_data, httpcode=403) + page_data = self.parser.Parse('403.html', error=command) + return uweb3.Response(content=page_data, httpcode=403, headers=self.req.response.headers) @classmethod def LoadModules(cls, default_routes='routes', excluded_files=('__init__', '.pyc')): @@ -291,14 +346,13 @@ def CommonBlocks(self, title, page_id=None, scripts=None): #TODO: self.user is no more return {'header': self.parser.Parse( - 'header.html', title=title, page_id=page_id, user=self.user + 'header.html', title=title, page_id=page_id ), 'footer': self.parser.Parse( - 'footer.html', year=time.strftime('%Y'), user=self.user, + 'footer.html', year=time.strftime('%Y'), page_id=page_id, scripts=scripts ), 'page_id': page_id, - 'xsrftoken': self._GetXSRF(), } diff --git a/uweb3/pagemaker/new_decorators.py b/uweb3/pagemaker/new_decorators.py index 569a91d3..92a766b5 100644 --- a/uweb3/pagemaker/new_decorators.py +++ b/uweb3/pagemaker/new_decorators.py @@ -29,74 +29,10 @@ def checkxsrf(f): (post) request. Make sure to have xsrf_enabled = True in the config.ini """ def wrapper(*args, **kwargs): - #TODO: How do we supply the seed for generating an XSRF token? - #In this case we use the user_id but how do we get it - args[0].user = {'user_id': '1'} - - xsrf_cookie = args[0].cookies.get('xsrf') - xsrf = XSRF(args[0].req.AddCookie, args[0].post) - if args[0].req.method == "GET": - if not xsrf_cookie: - #If the cookie doesn't exist generate a token and add it in a cookie - args[0].xsrf = xsrf.Generate_xsrf_token(args[0].user.get('user_id')) - args[0].req.AddCookie('xsrf', args[0].xsrf) - else: - #If the cookie exists but the xsrf is not valid replace the cookie with a valid one - if not xsrf.is_valid_xsrf_token(args[0].user.get('user_id')): - args[0].xsrf = xsrf.Generate_xsrf_token(args[0].user.get('user_id')) - args[0].req.AddCookie('xsrf', args[0].xsrf) - else: - args[0].xsrf = xsrf_cookie - else: - #On a post request check if there is a cookie with xsrf and if the post contains an xsrf input - if not xsrf_cookie: - args = clear_form_data(*args) - return args[0].XSRFInvalidToken('XSRF cookie is missing') - if not args[0].post.get('xsrf'): - args = clear_form_data(*args) - return args[0].XSRFInvalidToken('XSRF token is missing') - #Validate token - if not xsrf.is_valid_xsrf_token(args[0].user.get('user_id')): + if args[0].req.method != "GET": + if args[0].invalid_xsrf_token: args = clear_form_data(*args) - return args[0].XSRFInvalidToken('XSRF token is not valid') - args[0].xsrf = xsrf_cookie + return args[0].XSRFInvalidToken('XSRF token is invalid or missing') return f(*args, **kwargs) return wrapper - -class XSRF(object): - # secret = str(os.urandom(64)) - secret = "test" - def __init__(self, AddCookie, post): - """Checks if cookie with xsrf key is present. - - If not generates xsrf token and places it in a cookie. - Checks if xsrf token in post is equal to the one in the cookie and returns - True when they do not match and False when they do match for the 'incorrect_xsrf_token' flag. - """ - self.unix_timestamp = time.mktime(datetime.datetime.now().date().timetuple()) - self.AddCookie = AddCookie - self.post = post - - def is_valid_xsrf_token(self, userid): - """Validate given xsrf token based on userid - - Arguments: - @ userid: str/int - - Returns: - IsValid: boolean - """ - token = self.Generate_xsrf_token(userid) - if not self.post.get('xsrf'): - return False - if self.post.get('xsrf') != token: - return False - return True - - def Generate_xsrf_token(self, userid): - hashed = (str(self.unix_timestamp) + self.secret + userid).encode('utf-8') - h = hashlib.new('ripemd160') - h.update(hashed) - return h.hexdigest() - diff --git a/uweb3/request.py b/uweb3/request.py index 48e57bd4..d93c6773 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -98,7 +98,6 @@ def __init__(self, env, registry): 'delete': PostDictionary(), } self.env['host'] = self.headers.get('Host', '') - if self.method == 'POST': stream, form, files = parse_form_data(self.env) if self.env['CONTENT_TYPE'] == 'application/json': From 7d2a2aecf79cbcacc07466577588f5f30112a7f5 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 20 May 2020 10:51:17 +0200 Subject: [PATCH 021/118] Added Storage class to the PageMaker. PageMaker now supports self.Flash to flash messages to the template and self.ExtendTemplate() --- uweb3/pagemaker/__init__.py | 24 +++++++++++++++++++++--- uweb3/templateparser.py | 11 +++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 738b306e..d39066cd 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -162,8 +162,22 @@ def is_valid(self, supplied_token): token = self.generate_token() return token != supplied_token +class Storage(object): + def __init__(self): + self.storage = {} + self.messages = [] + self.extended_templates = {} + + def Flash(self, message): + self.messages.append(message) + + def ExtendTemplate(self, title, template, **kwds): + if self.extended_templates.get(title): + raise ValueError("There is already a template with this title") + self.extended_templates[title] = self.parser.Parse(template, **kwds) + -class BasePageMaker(object): +class BasePageMaker(Storage): """Provides the base pagemaker methods for all the html generators.""" # Constant for persistent storage accross requests. This will be accessible # by all threads of the same application (in the same Python process). @@ -203,6 +217,7 @@ def __init__(self, self.persistent = self.PERSISTENT self.secure_cookie_connection = (self.req, self.cookies, secure_cookie_secret) self.set_invalid_xsrf_token_flag(XSRF_seed) + super(BasePageMaker, self).__init__() def set_invalid_xsrf_token_flag(self, XSRF_seed): """Sets the invalid_xsrf_token flag to true or false""" @@ -318,7 +333,11 @@ def parser(self): if '__parser' not in self.persistent: self.persistent.Set('__parser', templateparser.Parser( self.options.get('templates', {}).get('path', self.TEMPLATE_DIR))) - return self.persistent.Get('__parser') + parser = self.persistent.Get('__parser') + parser.messages = self.messages + parser.templates = self.extended_templates + parser.storage = self.storage + return parser def InternalServerError(self, exc_type, exc_value, traceback): """Returns a plain text notification about an internal server error.""" @@ -344,7 +363,6 @@ def CommonBlocks(self, title, page_id=None, scripts=None): if not page_id: page_id = title.replace(' ', '_').lower() - #TODO: self.user is no more return {'header': self.parser.Parse( 'header.html', title=title, page_id=page_id ), diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index 7a2c75e6..5022a47e 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -139,9 +139,15 @@ def __init__(self, path='.', templates=(), noparse=False): super(Parser, self).__init__() self.template_dir = path self.noparse = noparse + + self.messages = None + self.templates = None + self.storage = None + for template in templates: self.AddTemplate(template) + def __getitem__(self, template): """Retrieves a stored template by name. @@ -201,6 +207,11 @@ def Parse(self, template, **replacements): Returns: str: The template with relevant tags replaced by the replacement dict. """ + + replacements['messages'] = self.messages + replacements['storage'] = self.storage + replacements.update(self.templates) + print(replacements) return self[template].Parse(**replacements) def ParseString(self, template, **replacements): From f4af5234264892952734ac5e379afd2be4420e4a Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Wed, 20 May 2020 11:04:11 +0200 Subject: [PATCH 022/118] uWeb3 now searches for a PreRequest method in the PageMaker. PreRequest should be used for self.ExtendTemplate methods --- uweb3/__init__.py | 4 ++++ uweb3/pagemaker/__init__.py | 6 +++++- uweb3/templateparser.py | 1 - 3 files changed, 9 insertions(+), 2 deletions(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index a6e63098..93a44617 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -203,6 +203,10 @@ def __call__(self, env, start_response): secure_cookie_secret=self.secure_cookie_secret, executing_path=self.executing_path, XSRF_seed=self.XSRF_seed) + + if hasattr(pagemaker, '_PreRequest'): + pagemaker = pagemaker._PreRequest() + response = self.get_response(pagemaker, method, args) except NoRouteError: #When we catch this error this means there is no method for the expected function diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index d39066cd..013222f1 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -247,6 +247,11 @@ def _Set_XSRF_cookie(self, XSRF_seed): self.req.AddCookie("xsrf", XSRF(XSRF_seed, self.req.env['REAL_REMOTE_ADDR']).generate_token()) return + def _PreRequest(self): + # self.ExtendTemplate("footer", "footer.html") + # self.ExtendTemplate("header", "header.html") + return self + def _PostRequest(self, response): if response.status == '500 Internal Server Error': if not hasattr(self, 'connection_error'): #this is set when we try and create a connection but it failed @@ -261,7 +266,6 @@ def _PostRequest(self, response): self.connection.close() self.persistent.Del("__mysql") self.connection_error = False - return response def XSRFInvalidToken(self, command): diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index 5022a47e..5dac7757 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -211,7 +211,6 @@ def Parse(self, template, **replacements): replacements['messages'] = self.messages replacements['storage'] = self.storage replacements.update(self.templates) - print(replacements) return self[template].Parse(**replacements) def ParseString(self, template, **replacements): From 1b2e85558685413d73a65912a227ff7834f75385 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Fri, 22 May 2020 09:47:01 +0200 Subject: [PATCH 023/118] Updated decorators --- uweb3/pagemaker/__init__.py | 5 ----- uweb3/pagemaker/decorators.py | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 013222f1..ef3515e2 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -247,11 +247,6 @@ def _Set_XSRF_cookie(self, XSRF_seed): self.req.AddCookie("xsrf", XSRF(XSRF_seed, self.req.env['REAL_REMOTE_ADDR']).generate_token()) return - def _PreRequest(self): - # self.ExtendTemplate("footer", "footer.html") - # self.ExtendTemplate("header", "header.html") - return self - def _PostRequest(self, response): if response.status == '500 Internal Server Error': if not hasattr(self, 'connection_error'): #this is set when we try and create a connection but it failed diff --git a/uweb3/pagemaker/decorators.py b/uweb3/pagemaker/decorators.py index 8397fd1e..0aeeb40a 100644 --- a/uweb3/pagemaker/decorators.py +++ b/uweb3/pagemaker/decorators.py @@ -43,7 +43,7 @@ def wrapper(*args, **kwargs): from datetime import datetime import time import pickle -from underdarkcustomers import model +from uweb3 import model def Cached(maxage=None, verbose=False, *t_args, **t_kwargs): """Decorator that wraps checks the cache table for a cached page. From b001f3ffa5ef3a89c0fa2a0b8e1a236eea8fd059 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Fri, 22 May 2020 09:50:00 +0200 Subject: [PATCH 024/118] Decorators now use the correct uweb version --- uweb3/pagemaker/decorators.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uweb3/pagemaker/decorators.py b/uweb3/pagemaker/decorators.py index 0aeeb40a..5d4f18a4 100644 --- a/uweb3/pagemaker/decorators.py +++ b/uweb3/pagemaker/decorators.py @@ -8,7 +8,7 @@ def loggedin(f): def wrapper(*args, **kwargs): try: args[0].user = args[0]._CurrentUser() - except (uweb.model.NotExistError, args[0].NoSessionError): + except (uweb3.model.NotExistError, args[0].NoSessionError): path = '/login' if args[0].req.env['PATH_INFO'].strip() != '': path = '%s/%s' % (path, args[0].req.env['PATH_INFO'].strip()) @@ -106,7 +106,7 @@ def wrapper(*args, **kwargs): data = pickle.loads(str(data['data'])) except NameError: create = True - except uweb.model.NotExistError: # we don't have anything fresh enough, lets create + except uweb3.model.NotExistError: # we don't have anything fresh enough, lets create create = True if create: try: @@ -140,7 +140,7 @@ def TemplateParser(template, *t_args, **t_kwargs): def template_decorator(f): def wrapper(*args, **kwargs): pageresult = f(*args, **kwargs) or {} - if not isinstance(pageresult, (str, uweb.Response, uweb.Redirect)): + if not isinstance(pageresult, (str, uweb3.Response, uweb3.Redirect)): pageresult.update(args[0].CommonBlocks(*t_args, **t_kwargs)) return args[0].parser.Parse(template, **pageresult) return pageresult From eaa6bb3a339d0c24c6bd6729f11b2f6d7b187ad7 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Mon, 25 May 2020 09:34:56 +0200 Subject: [PATCH 025/118] Moved SQLAlchemy into seperate file, removed leftovers in model --- uweb3/alchemy_model.py | 166 +++++++++++++++- uweb3/model.py | 348 ---------------------------------- uweb3/pagemaker/decorators.py | 17 +- 3 files changed, 171 insertions(+), 360 deletions(-) diff --git a/uweb3/alchemy_model.py b/uweb3/alchemy_model.py index df042d57..19b44a0b 100644 --- a/uweb3/alchemy_model.py +++ b/uweb3/alchemy_model.py @@ -1,10 +1,13 @@ -from uweb3.model import NotExistError +from itertools import chain + from sqlalchemy import Column, Integer, String from sqlalchemy.ext.declarative import declarative_base, declared_attr -from sqlalchemy.orm import sessionmaker, reconstructor -from sqlalchemy.orm.session import object_session from sqlalchemy.inspection import inspect -from itertools import chain +from sqlalchemy.orm import reconstructor, sessionmaker +from sqlalchemy.orm.session import object_session + +from uweb3.model import NotExistError + class AlchemyBaseRecord(object): def __init__(self, session, record): @@ -345,4 +348,157 @@ def Save(self): """Saves any changes made in the current record. Sqlalchemy automatically detects these changes and only updates the changed values. If no values are present no query will be commited.""" - self.session.commit() \ No newline at end of file + self.session.commit() + + +class AlchemyRecord(AlchemyBaseRecord): + """ """ + @classmethod + def FromPrimary(cls, session, p_key): + """Finds record based on given class and supplied primary key. + + Arguments: + @ session: sqlalchemy session object + Available in the pagemaker with self.session + @ p_key: integer + primary_key of the object to delete + Returns + cls + None + """ + try: + record = session.query(cls).filter(cls._PrimaryKeyCondition(cls) == p_key).first() + except: + session.rollback() + raise + else: + if not record: + raise NotExistError(f"Record with primary key {p_key} does not exist") + return record + + @classmethod + def DeletePrimary(cls, session, p_key): + """Deletes record base on primary key from given class. + + Arguments: + @ session: sqlalchemy session object + Available in the pagemaker with self.session + @ p_key: integer + primary_key of the object to delete + + Returns: + isdeleted: boolean + """ + try: + isdeleted = session.query(cls).filter(cls._PrimaryKeyCondition(cls) == p_key).delete() + except: + session.rollback() + raise + else: + session.commit() + return isdeleted + + @classmethod + def Create(cls, session, record): + """Creates a new instance and commits it to the database + + Arguments: + @ session: sqlalchemy session object + Available in the pagemaker with self.session + @ record: dict + Dictionary with all key:value pairs that are required for the db record + Returns: + cls + """ + return cls(session, record) + + @classmethod + def List(cls, session, conditions=None, limit=None, offset=None, + order=None, yield_unlimited_total_first=False): + """Yields a Record object for every table entry. + + Arguments: + @ session: sqlalchemy session object + Available in the pagemaker with self.session + % conditions: list + Optional query portion that will be used to limit the list of results. + If multiple conditions are provided, they are joined on an 'AND' string. + For example: conditions=[User.id <= 10, User.id >=] + % limit: int ~~ None + Specifies a maximum number of items to be yielded. The limit happens on + the database side, limiting the query results. + % offset: int ~~ None + Specifies the offset at which the yielded items should start. Combined + with limit this enables proper pagination. + % order: tuple of operants + For example the User class has 3 fields; id, username, password. We can pass + the field we want to order on to the tuple like so; + (User.id.asc(), User.username.desc()) + % yield_unlimited_total_first: bool ~~ False + Instead of yielding only Record objects, the first item returned is the + number of results from the query if it had been executed without limit. + + Returns: + integer: integer with length of results. + list: List of classes from request type + """ + try: + query = session.query(cls) + if conditions: + for condition in conditions: + query = query.filter(condition) + if order: + for item in order: + query = query.order_by(item) + if limit: + query = query.limit(limit) + if offset: + query = query.offset(offset) + result = query.all() + except: + session.rollback() + raise + else: + if yield_unlimited_total_first: + return len(result) + return result + + @classmethod + def Update(cls, session, conditions, values): + """Update table based on conditions. + + Arguments: + @ session: sqlalchemy session object + Available in the pagemaker with self.session + @ conditions: list|tuple + for example: [User.id > 2, User.id < 100] + @ values: dict + for example: {User.username: 'value'} + """ + try: + query = session.query(cls) + for condition in conditions: + query = query.filter(condition) + query = query.update(values) + except: + session.rollback() + raise + else: + session.commit() + + def Delete(self): + """Delete current instance from the database""" + try: + isdeleted = self.session.query(type(self)).filter(self._PrimaryKeyCondition(self) == self.key).delete() + except: + self.session.rollback() + raise + else: + self.session.commit() + return isdeleted + + def Save(self): + """Saves any changes made in the current record. Sqlalchemy automatically detects + these changes and only updates the changed values. If no values are present + no query will be commited.""" + self.session.commit() diff --git a/uweb3/model.py b/uweb3/model.py index 3f74920a..164cf982 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -11,14 +11,8 @@ import secrets import configparser -from sqlalchemy import Column, Integer, String -from sqlalchemy.ext.declarative import declarative_base, declared_attr -from sqlalchemy.orm import sessionmaker, reconstructor -from sqlalchemy.orm.session import object_session -from sqlalchemy.inspection import inspect from contextlib import contextmanager -from itertools import chain class Error(Exception): """Superclass used for inheritance and external exception handling.""" @@ -1309,348 +1303,6 @@ def Save(self): def _StoreRecord(self): self.key = self.Collection(self.connection).save(self._DataRecord()) - -class AlchemyBaseRecord(object): - def __init__(self, session, record): - self.session = session - self._BuildClassFromRecord(record) - - def _BuildClassFromRecord(self, record): - if isinstance(record, dict): - for key, value in record.items(): - if not key in self.__table__.columns.keys(): - raise AttributeError(f"Key '{key}' not specified in class '{self.__class__.__name__}'") - setattr(self, key, value) - if self.session: - try: - self.session.add(self) - except: - self.session.rollback() - raise - else: - self.session.commit() - - def __hash__(self): - """Returns the hashed value of the key.""" - return hash(self.key) - - def __del__(self): - """Cleans up the connection at the end of its life cycle""" - self.session.close() - - def __repr__(self): - s = {} - for key in self.__table__.columns.keys(): - value = getattr(self, key) - if value: - s[key] = value - return f'{type(self).__name__}({s})' - - def __eq__(self, other): - if type(self) != type(other): - return False # Types must be the same. - elif not (self.key == other.key is not None): - return False # Records should have the same non-None primary key value. - elif len(self) != len(other): - return False # Records must contain the same number of objects. - for key in self.__table__.columns.keys(): - value = getattr(self, key) - other_value = getattr(other, key) - if isinstance(self, AlchemyBaseRecord) != isinstance(other, AlchemyBaseRecord): - # Only one of the two is a BaseRecord instance - if (isinstance(self, AlchemyBaseRecord) and value.key != other_value or - isinstance(other, AlchemyBaseRecord) and other_value.key != value): - return False - elif value != other_value: - return False - return True - - def __ne__(self, other): - """Returns the proper inverse of __eq__.""" - # Without this, the non-equal checks used in __eq__ will not work, - # and the `!=` operator would not be the logical inverse of `==`. - return not self == other - - def __len__(self): - return len(dict((col, getattr(self, col)) for col in self.__table__.columns.keys() if getattr(self, col))) - - def __int__(self): - """Returns the integer key value of the Record. - - For record objects where the primary key value is not (always) an integer, - this function will raise an error in the situations where it is not. - """ - key_val = self.key - if not isinstance(key_val, (int)): - # We should not truncate floating point numbers. - # Nor turn strings of numbers into an integer. - raise ValueError('The primary key is not an integral number.') - return key_val - - def copy(self): - """Returns a shallow copy of the Record that is a new functional Record.""" - import copy - return copy.copy(self) - - def deepcopy(self): - import copy - return copy.deepcopy(self) - - def __gt__(self, other): - """Index of this record is greater than the other record's. - - This requires both records to be of the same record class. - """ - if type(self) == type(other): - return self.key > other.key - return NotImplemented - - def __ge__(self, other): - """Index of this record is greater than, or equal to, the other record's. - - This requires both records to be of the same record class. - """ - if type(self) == type(other): - return self.key >= other.key - return NotImplemented - - def __lt__(self, other): - """Index of this record is smaller than the other record's. - - This requires both records to be of the same record class. - """ - if type(self) == type(other): - return self.key < other.key - return NotImplemented - - def __le__(self, other): - """Index of this record is smaller than, or equal to, the other record's. - - This requires both records to be of the same record class. - """ - if type(self) == type(other): - return self.key <= other.key - return NotImplemented - - def __getitem__(self, field): - return getattr(self, field) - - def iteritems(self): - """Yields all field+value pairs in the Record. - - This automatically loads in relationships. - """ - return chain(((key, getattr(self, key)) for key in self.__table__.columns.keys()), - ((child[0], getattr(self, child[0])) for child in inspect(type(self)).relationships.items())) - - def itervalues(self): - """Yields all values in the Record, loads relationships""" - return chain((getattr(self, key) for key in self.__table__.columns.keys()), - (getattr(self, child[0]) for child in inspect(type(self)).relationships.items())) - - def items(self): - """Returns a list of field+value pairs in the Record. - - This automatically loads in relationships. - """ - return list(self.iteritems()) - - def values(self): - """Returns a list of values in the Record, loading foreign references.""" - return list(self.itervalues()) - - @property - def key(self): - return getattr(self, inspect(type(self)).primary_key[0].name) - - @classmethod - def TableName(cls): - """Returns the database table name for the Record class.""" - return cls.__tablename__ - - @classmethod - def _AlchemyRecordToDict(cls, record): - """Turns the values of a given class into a dictionary. Doesn't trigger - automatic loading of child classes. - - Arguments: - @ record: cls - AlchemyBaseRecord class that is retrieved from a database query - Returns - dict: dictionary with all table columns and values - None: when record is empty - """ - if not isinstance(record, type(None)): - return dict((col, getattr(record, col)) for col in record.__table__.columns.keys()) - return None - - @reconstructor - def reconstruct(self): - """This is called instead of __init__ when the result comes from the database""" - self.session = object_session(self) - - @classmethod - def _PrimaryKeyCondition(cls, target): - """Returns the name of the primary key of given class - - Arguments: - @ target: cls - Class that you want to know the primary key name from - """ - return getattr(cls, inspect(cls).primary_key[0].name) - -class AlchemyRecord(AlchemyBaseRecord): - """ """ - @classmethod - def FromPrimary(cls, session, p_key): - """Finds record based on given class and supplied primary key. - - Arguments: - @ session: sqlalchemy session object - Available in the pagemaker with self.session - @ p_key: integer - primary_key of the object to delete - Returns - cls - None - """ - try: - record = session.query(cls).filter(cls._PrimaryKeyCondition(cls) == p_key).first() - except: - session.rollback() - raise - else: - if not record: - raise NotExistError(f"Record with primary key {p_key} does not exist") - return record - - @classmethod - def DeletePrimary(cls, session, p_key): - """Deletes record base on primary key from given class. - - Arguments: - @ session: sqlalchemy session object - Available in the pagemaker with self.session - @ p_key: integer - primary_key of the object to delete - - Returns: - isdeleted: boolean - """ - try: - isdeleted = session.query(cls).filter(cls._PrimaryKeyCondition(cls) == p_key).delete() - except: - session.rollback() - raise - else: - session.commit() - return isdeleted - - @classmethod - def Create(cls, session, record): - """Creates a new instance and commits it to the database - - Arguments: - @ session: sqlalchemy session object - Available in the pagemaker with self.session - @ record: dict - Dictionary with all key:value pairs that are required for the db record - Returns: - cls - """ - return cls(session, record) - - @classmethod - def List(cls, session, conditions=None, limit=None, offset=None, - order=None, yield_unlimited_total_first=False): - """Yields a Record object for every table entry. - - Arguments: - @ session: sqlalchemy session object - Available in the pagemaker with self.session - % conditions: list - Optional query portion that will be used to limit the list of results. - If multiple conditions are provided, they are joined on an 'AND' string. - For example: conditions=[User.id <= 10, User.id >=] - % limit: int ~~ None - Specifies a maximum number of items to be yielded. The limit happens on - the database side, limiting the query results. - % offset: int ~~ None - Specifies the offset at which the yielded items should start. Combined - with limit this enables proper pagination. - % order: tuple of operants - For example the User class has 3 fields; id, username, password. We can pass - the field we want to order on to the tuple like so; - (User.id.asc(), User.username.desc()) - % yield_unlimited_total_first: bool ~~ False - Instead of yielding only Record objects, the first item returned is the - number of results from the query if it had been executed without limit. - - Returns: - integer: integer with length of results. - list: List of classes from request type - """ - try: - query = session.query(cls) - if conditions: - for condition in conditions: - query = query.filter(condition) - if order: - for item in order: - query = query.order_by(item) - if limit: - query = query.limit(limit) - if offset: - query = query.offset(offset) - result = query.all() - except: - session.rollback() - raise - else: - if yield_unlimited_total_first: - return len(result) - return result - - @classmethod - def Update(cls, session, conditions, values): - """Update table based on conditions. - - Arguments: - @ session: sqlalchemy session object - Available in the pagemaker with self.session - @ conditions: list|tuple - for example: [User.id > 2, User.id < 100] - @ values: dict - for example: {User.username: 'value'} - """ - try: - query = session.query(cls) - for condition in conditions: - query = query.filter(condition) - query = query.update(values) - except: - session.rollback() - raise - else: - session.commit() - - def Delete(self): - """Delete current instance from the database""" - try: - isdeleted = self.session.query(type(self)).filter(self._PrimaryKeyCondition(self) == self.key).delete() - except: - self.session.rollback() - raise - else: - self.session.commit() - return isdeleted - - def Save(self): - """Saves any changes made in the current record. Sqlalchemy automatically detects - these changes and only updates the changed values. If no values are present - no query will be commited.""" - self.session.commit() - class Smorgasbord(object): """A connection tracker for uWeb3 Record classes. diff --git a/uweb3/pagemaker/decorators.py b/uweb3/pagemaker/decorators.py index 5d4f18a4..7ca48290 100644 --- a/uweb3/pagemaker/decorators.py +++ b/uweb3/pagemaker/decorators.py @@ -1,7 +1,16 @@ """This file holds all the decorators we use in this project.""" -import uweb3 +import pickle +import time +from datetime import datetime + +import pytz +import simplejson + import _mysql_exceptions +import uweb3 +from uweb3 import model + def loggedin(f): """Decorator that checks if the user requesting the page is logged in.""" @@ -38,12 +47,6 @@ def wrapper(*args, **kwargs): return f(*args, **kwargs) return wrapper -import simplejson -import pytz -from datetime import datetime -import time -import pickle -from uweb3 import model def Cached(maxage=None, verbose=False, *t_args, **t_kwargs): """Decorator that wraps checks the cache table for a cached page. From 8ddbe77b237f79dc061c76f128f3a972e3c28076 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Mon, 25 May 2020 13:47:12 +0200 Subject: [PATCH 026/118] updated uweb3 cached decorator to be python3 compatible --- uweb3/model.py | 61 +++++++++++++++++++++++++++++++++++ uweb3/pagemaker/__init__.py | 4 +-- uweb3/pagemaker/decorators.py | 38 +++++++++++----------- 3 files changed, 82 insertions(+), 21 deletions(-) diff --git a/uweb3/model.py b/uweb3/model.py index 164cf982..7eb865db 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -21,6 +21,8 @@ class Error(Exception): class DatabaseError(Error): """Superclass for errors returned by the database backend.""" +class CurrentlyWorking(Error): + """Caching error""" class BadFieldError(DatabaseError): """A field in the record could not be written to the database.""" @@ -1462,3 +1464,62 @@ def _Encode(obj): record = RecordToDict(record, complete=complete, recursive=recursive) return simplejson.dumps( record, default=_Encode, sort_keys=True, indent=indent) + + +import functools + +class CachedPage(Record): + """Abstraction class for the cached Pages table in the database.""" + + MAXAGE = 61 + + @classmethod + def Clean(cls, connection, maxage=None): + """Deletes all cached pages that are older than MAXAGE. + + An optional 'maxage' integer can be specified instead of MAXAGE. + """ + with connection as cursor: + cursor.Execute("""delete + from + %s + where + TIME_TO_SEC(TIMEDIFF(UTC_TIMESTAMP(), created)) > %d + """ % ( + cls.TableName(), + (cls.MAXAGE if maxage is None else maxage) + )) + + @classmethod + def FromSignature(cls, connection, maxage, name, modulename, args, kwargs): + """Returns a cached page from the given signature.""" + with connection as cursor: + cache = cursor.Execute("""select + data, + TIME_TO_SEC(TIMEDIFF(UTC_TIMESTAMP(), created)) as age, + creating + from + %s + where + TIME_TO_SEC(TIMEDIFF(UTC_TIMESTAMP(), created)) < %d AND + name = %s AND + modulename = %s AND + args = %s AND + kwargs = %s + order by created desc + limit 1 + """ % ( + cls.TableName(), + (cls.MAXAGE if maxage is None else maxage), + connection.EscapeValues(name), + connection.EscapeValues(modulename), + connection.EscapeValues(args), + connection.EscapeValues(kwargs))) + + if cache: + if cache[0]['creating'] is not None: + raise CurrentlyWorking(cache[0]['age']) + return cls(connection, cache[0]) + else: + raise cls.NotExistError('No cached data found') + diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index ef3515e2..f33eb7f3 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -206,6 +206,7 @@ def __init__(self, Configuration for the pagemaker, with database connection information and other settings. This will be available through `self.options`. """ + super(BasePageMaker, self).__init__() self.__SetupPaths(executing_path) self.req = req self.cookies = req.vars['cookie'] @@ -217,7 +218,6 @@ def __init__(self, self.persistent = self.PERSISTENT self.secure_cookie_connection = (self.req, self.cookies, secure_cookie_secret) self.set_invalid_xsrf_token_flag(XSRF_seed) - super(BasePageMaker, self).__init__() def set_invalid_xsrf_token_flag(self, XSRF_seed): """Sets the invalid_xsrf_token flag to true or false""" @@ -563,7 +563,7 @@ def _LoadMongo(self): def _LoadRelational(self): """Returns the PageMaker's relational database connection.""" - return self.pagemaker.connection + return self.pagemaker.connectionPageMaker @property def bord(self): diff --git a/uweb3/pagemaker/decorators.py b/uweb3/pagemaker/decorators.py index 7ca48290..29bb5a8a 100644 --- a/uweb3/pagemaker/decorators.py +++ b/uweb3/pagemaker/decorators.py @@ -6,11 +6,11 @@ import pytz import simplejson - -import _mysql_exceptions +import codecs +# import _mysql_exceptions import uweb3 from uweb3 import model - +from pymysql import err def loggedin(f): """Decorator that checks if the user requesting the page is logged in.""" @@ -47,52 +47,50 @@ def wrapper(*args, **kwargs): return f(*args, **kwargs) return wrapper -def Cached(maxage=None, verbose=False, *t_args, **t_kwargs): +def Cached(maxage=None, verbose=False, handler=None, *t_args, **t_kwargs): """Decorator that wraps checks the cache table for a cached page. - The function will see if we have a recent cached output for this call, or if one is being created as we speak. - Use by adding the decorator module and flagging a pagemaker function with it. - from pages import decorators @decorators.Cached(60) def mypage() - Arguments: maxage: int(60), cache time in seconds. verbose: bool(false), insert html comment with cache information. - """ def cache_decorator(f): def wrapper(*args, **kwargs): + print() create = False name = f.__name__ modulename = f.__module__ - model.CachedPage.Clean(args[0].connection, maxage) + # model.CachedPage.Clean(args[0].connection, maxage) + handler.Clean(args[0].connection, maxage) requesttime = time.time() time.clock() sleep = 0.3 try: # see if we have a cached version thats not too old - data = model.CachedPage.FromSignature(args[0].connection, + data = handler.FromSignature(args[0].connection, maxage, name, modulename, simplejson.dumps(args[1:]), simplejson.dumps(kwargs)) if verbose: data = '%s' % ( - pickle.loads(str(data['data'])), + pickle.loads(codecs.decode(data['data'].encode(), "base64")), data['age']) else: - data = pickle.loads(str(data['data'])) + data = pickle.loads(codecs.decode(data['data'].encode(), "base64")) + print("LOAD FROM CACHE") except model.CurrentlyWorking: # we dont have anything fresh enough, but someones working on it age = 0 while age < maxage: # as long as there's no output, we should try periodically until we have waited too long time.sleep(sleep) age = (time.time() - requesttime) try: - data = model.CachedPage.FromSignature(args[0].connection, + data = handler.FromSignature(args[0].connection, maxage, name, modulename, simplejson.dumps(args[1:]), @@ -103,17 +101,18 @@ def wrapper(*args, **kwargs): try: if verbose: data = '%s' % ( - pickle.loads(str(data['data'])), + pickle.loads(codecs.decode(data['data'].encode(), "base64")), age) else: - data = pickle.loads(str(data['data'])) + data = pickle.loads(codecs.decode(data['data'].encode(), "base64")) except NameError: create = True except uweb3.model.NotExistError: # we don't have anything fresh enough, lets create create = True if create: + print("CREATE NEW CACHE") try: - cache = model.CachedPage.Create(args[0].connection, { + cache = handler.Create(args[0].connection, { 'name': name, 'modulename': modulename, 'args': simplejson.dumps(args[1:]), @@ -122,13 +121,14 @@ def wrapper(*args, **kwargs): 'created': str(pytz.utc.localize(datetime.utcnow()))[0:19] }) data = f(*args, **kwargs) - cache['data'] = pickle.dumps(data) + cache['data'] = codecs.encode(pickle.dumps(data), "base64").decode() cache['created'] = str(pytz.utc.localize(datetime.utcnow()))[0:19] cache['creating'] = None cache.Save() if verbose: data = '%s' % data - except _mysql_exceptions.OperationalError: + # except _mysql_exceptions.OperationalError: + except Exception: pass return data return wrapper From f34e4c444fdf7fa0288baa5ef0871f1b739a2b81 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Mon, 25 May 2020 13:52:43 +0200 Subject: [PATCH 027/118] model.CachedPage now inherits from object so the user has more control over which record to use --- uweb3/model.py | 2 +- uweb3/pagemaker/decorators.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/uweb3/model.py b/uweb3/model.py index 7eb865db..c3355cec 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -1468,7 +1468,7 @@ def _Encode(obj): import functools -class CachedPage(Record): +class CachedPage(object): """Abstraction class for the cached Pages table in the database.""" MAXAGE = 61 diff --git a/uweb3/pagemaker/decorators.py b/uweb3/pagemaker/decorators.py index 29bb5a8a..7d1a0184 100644 --- a/uweb3/pagemaker/decorators.py +++ b/uweb3/pagemaker/decorators.py @@ -62,7 +62,6 @@ def mypage() """ def cache_decorator(f): def wrapper(*args, **kwargs): - print() create = False name = f.__name__ modulename = f.__module__ @@ -83,7 +82,6 @@ def wrapper(*args, **kwargs): data['age']) else: data = pickle.loads(codecs.decode(data['data'].encode(), "base64")) - print("LOAD FROM CACHE") except model.CurrentlyWorking: # we dont have anything fresh enough, but someones working on it age = 0 while age < maxage: # as long as there's no output, we should try periodically until we have waited too long @@ -110,7 +108,6 @@ def wrapper(*args, **kwargs): except uweb3.model.NotExistError: # we don't have anything fresh enough, lets create create = True if create: - print("CREATE NEW CACHE") try: cache = handler.Create(args[0].connection, { 'name': name, From d03931b725a16e32b6627637ee39e6c7e6aff7af Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Thu, 28 May 2020 10:04:21 +0200 Subject: [PATCH 028/118] Added docstrings to the Flash and ExtendTemplate --- uweb3/pagemaker/__init__.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index f33eb7f3..f0f7b761 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -169,9 +169,28 @@ def __init__(self): self.extended_templates = {} def Flash(self, message): + """Appends message to list, list element is vailable in the template under keyword messages + + Arguments: + @ message: str + Raises: + TypeError + """ + if not isinstance(message, str): + raise TypeError("Message is of incorrect type, Should be string.") self.messages.append(message) def ExtendTemplate(self, title, template, **kwds): + """Extend the template on which this method is called. + + Arguments: + @ title: str + Name of the variable that you can access the extended template at + @ template: str + Name of the template that you want to extend + % **kwds: kwds + The keywords that you want to pass to the template. Works the same as self.parser.Parse('template.html', var=value) + """ if self.extended_templates.get(title): raise ValueError("There is already a template with this title") self.extended_templates[title] = self.parser.Parse(template, **kwds) From 83c0016b967149593ca3980e4169db9bad187de5 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Thu, 28 May 2020 10:15:19 +0200 Subject: [PATCH 029/118] Updated Cached decorator docstrings and added a error raise --- uweb3/pagemaker/decorators.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/uweb3/pagemaker/decorators.py b/uweb3/pagemaker/decorators.py index 7d1a0184..ba80c8d6 100644 --- a/uweb3/pagemaker/decorators.py +++ b/uweb3/pagemaker/decorators.py @@ -56,12 +56,22 @@ def Cached(maxage=None, verbose=False, handler=None, *t_args, **t_kwargs): from pages import decorators @decorators.Cached(60) def mypage() + Arguments: - maxage: int(60), cache time in seconds. - verbose: bool(false), insert html comment with cache information. + #TODO: Make handler an argument instead of a kwd since it is required? + @ handler: class CustomClass(model.Record, model.CachedPage) + This is some sort of custom mixin class that we use to store our cached page in the database + % maxage: int(60) + Cache time in seconds. + % verbose: bool(False) + Insert html comment with cache information. + Raises: + KeyError """ def cache_decorator(f): def wrapper(*args, **kwargs): + if not handler: + raise KeyError("A handler is required for storing this page into the database.") create = False name = f.__name__ modulename = f.__module__ From 446a999c5855124f90892ab84cd01833178fc1fd Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Thu, 28 May 2020 10:42:58 +0200 Subject: [PATCH 030/118] Updated the cached method, should now use the pymsql Error instead of Exception --- uweb3/pagemaker/decorators.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/uweb3/pagemaker/decorators.py b/uweb3/pagemaker/decorators.py index ba80c8d6..f04e8be5 100644 --- a/uweb3/pagemaker/decorators.py +++ b/uweb3/pagemaker/decorators.py @@ -10,7 +10,8 @@ # import _mysql_exceptions import uweb3 from uweb3 import model -from pymysql import err +from pymysql import Error + def loggedin(f): """Decorator that checks if the user requesting the page is logged in.""" @@ -75,7 +76,6 @@ def wrapper(*args, **kwargs): create = False name = f.__name__ modulename = f.__module__ - # model.CachedPage.Clean(args[0].connection, maxage) handler.Clean(args[0].connection, maxage) requesttime = time.time() time.clock() @@ -134,8 +134,7 @@ def wrapper(*args, **kwargs): cache.Save() if verbose: data = '%s' % data - # except _mysql_exceptions.OperationalError: - except Exception: + except Error: #This is a pymysql Error pass return data return wrapper From b8fccbd6677d133272e6e7fe9ef709e642b6942a Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Thu, 28 May 2020 10:50:01 +0200 Subject: [PATCH 031/118] Updated BasePageMaker docstrings --- uweb3/pagemaker/__init__.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index f0f7b761..403fde82 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -216,7 +216,8 @@ def __init__(self, secure_cookie_secret=None, executing_path=None, XSRF_seed=None): - """sets up the template parser and database connections + """sets up the template parser and database connections. + Handles setting the XSRF flag for each incoming request. Arguments: @ req: request.Request @@ -224,6 +225,12 @@ def __init__(self, % config: dict ~~ None Configuration for the pagemaker, with database connection information and other settings. This will be available through `self.options`. + % secure_cookie_secret: Randomly generated os.urandom(32) byte string + This is used as a secret for the SecureCookie class + % executing_path: str/path + This is the path to the uWeb3 routing file. + % XSRF_seed: Randomly generated os.urandom(32) byte string + This is used as a secret for the XSRF hash in the XSRF class. """ super(BasePageMaker, self).__init__() self.__SetupPaths(executing_path) From db1da4527d21cc1c4b954e24193e494637f70557 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Sat, 6 Jun 2020 10:20:21 +0200 Subject: [PATCH 032/118] Created a WebsocketPageMaker for websocket routes --- uweb3/__init__.py | 12 ++++++++++-- uweb3/pagemaker/__init__.py | 19 +++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 93a44617..1b7309d1 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -29,6 +29,7 @@ # Package classes from .response import Response from .pagemaker import PageMaker +from .pagemaker import WebsocketPageMaker from .pagemaker import DebuggingPageMaker from .pagemaker import SqAlchemyPageMaker from .helpers import StaticMiddleware @@ -82,6 +83,10 @@ def router(self, routes): request_router: Configured closure that processes urls. """ req_routes = [] + #Variable used to store websocket pagemakers, + #these pagemakers are only created at startup but can have multiple routes. + #To prevent creating the same instance for each route we store them in a dict + websocket_pagemaker = {} for pattern, *details in routes: pagemaker = None for pm in self.pagemakers: @@ -90,8 +95,11 @@ def router(self, routes): pagemaker = pm break if callable(pattern): - #TODO: Pass environment to a custom pagemaker for websockets? - pattern(getattr(pagemaker, details[0])) + #Check if the pagemaker is already in the dict, if not instantiate + #if so just use that one. This prevents creating multiple instances for one route. + if not websocket_pagemaker.get(pagemaker.__name__): + websocket_pagemaker[pagemaker.__name__] = pagemaker() + pattern(getattr(websocket_pagemaker[pagemaker.__name__], details[0])) continue if not pagemaker: raise NoRouteError(f"µWeb3 could not find a route handler called '{details[0]}' in any of your projects PageMakers.") diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 403fde82..3e7510b5 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -196,6 +196,19 @@ def ExtendTemplate(self, title, template, **kwds): self.extended_templates[title] = self.parser.Parse(template, **kwds) +class WebsocketPageMaker(object): + """Pagemaker for the websocket routes. + This is different from the BasePageMaker as we choose to not have a database connection + in our WebSocketPageMaker. + + This class lacks pretty much all functionality that the BasePageMaker has. + """ + + #TODO: What functions/methods need to be in this class + def __init__(self): + """ """ + print("called") + class BasePageMaker(Storage): """Provides the base pagemaker methods for all the html generators.""" # Constant for persistent storage accross requests. This will be accessible @@ -298,6 +311,12 @@ def XSRFInvalidToken(self, command): def LoadModules(cls, default_routes='routes', excluded_files=('__init__', '.pyc')): """Loops over all .py files apart from some exceptions in target directory Looks for classes that contain pagemaker + + Arguments: + % default_routes: str + Path to the directory where you want to store your routes. Defaults to routes. + % excluded_files: tuple(str) + Extension name of the files you want to exclude. Default excluded files are __init__ and .pyc. """ bases = [] routes = os.path.join(os.getcwd(), default_routes) From a61b1c97dd70ba54f8321c71489b6bb3c73fcf26 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Mon, 8 Jun 2020 09:55:40 +0200 Subject: [PATCH 033/118] WebsocketPagemaker can now access the uweb3 templateparser. --- uweb3/pagemaker/__init__.py | 60 ++++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 3e7510b5..8fac0e74 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -195,8 +195,35 @@ def ExtendTemplate(self, title, template, **kwds): raise ValueError("There is already a template with this title") self.extended_templates[title] = self.parser.Parse(template, **kwds) +class Base(object): + # Constant for persistent storage accross requests. This will be accessible + # by all threads of the same application (in the same Python process). + PERSISTENT = CacheStorage() + # Base paths for templates and public data. These are used in the PageMaker + # classmethods that set up paths specific for that pagemaker. + PUBLIC_DIR = 'static' + TEMPLATE_DIR = 'templates' + + @property + def parser(self): + """Provides a templateparser.Parser instance. -class WebsocketPageMaker(object): + If the config file specificied a [templates] section and a `path` is + assigned in there, this path will be used. + Otherwise, the `TEMPLATE_DIR` will be used to load templates from. + """ + if '__parser' not in self.persistent: + self.persistent.Set('__parser', templateparser.Parser( + self.options.get('templates', {}).get('path', self.TEMPLATE_DIR))) + parser = self.persistent.Get('__parser') + if hasattr(self, "messages"): + parser.messages = self.messages + parser.templates = self.extended_templates + parser.storage = self.storage + return parser + + +class WebsocketPageMaker(Base): """Pagemaker for the websocket routes. This is different from the BasePageMaker as we choose to not have a database connection in our WebSocketPageMaker. @@ -206,18 +233,12 @@ class WebsocketPageMaker(object): #TODO: What functions/methods need to be in this class def __init__(self): - """ """ - print("called") + #request + self.persistent = self.PERSISTENT -class BasePageMaker(Storage): + +class BasePageMaker(Base, Storage): """Provides the base pagemaker methods for all the html generators.""" - # Constant for persistent storage accross requests. This will be accessible - # by all threads of the same application (in the same Python process). - PERSISTENT = CacheStorage() - # Base paths for templates and public data. These are used in the PageMaker - # classmethods that set up paths specific for that pagemaker. - PUBLIC_DIR = 'static' - TEMPLATE_DIR = 'templates' _registery = [] # Default Static() handler cache durations, per MIMEtype, in days @@ -366,23 +387,6 @@ def __SetupPaths(cls, executing_path): cls.PUBLIC_DIR = os.path.join(cls_dir, cls.PUBLIC_DIR) cls.TEMPLATE_DIR = os.path.join(cls_dir, cls.TEMPLATE_DIR) - @property - def parser(self): - """Provides a templateparser.Parser instance. - - If the config file specificied a [templates] section and a `path` is - assigned in there, this path will be used. - Otherwise, the `TEMPLATE_DIR` will be used to load templates from. - """ - if '__parser' not in self.persistent: - self.persistent.Set('__parser', templateparser.Parser( - self.options.get('templates', {}).get('path', self.TEMPLATE_DIR))) - parser = self.persistent.Get('__parser') - parser.messages = self.messages - parser.templates = self.extended_templates - parser.storage = self.storage - return parser - def InternalServerError(self, exc_type, exc_value, traceback): """Returns a plain text notification about an internal server error.""" error = 'INTERNAL SERVER ERROR (HTTP 500) DURING PROCESSING OF %r' % ( From 98c042c15bb3de9589ae840cae27016e409f4400 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Mon, 8 Jun 2020 09:58:17 +0200 Subject: [PATCH 034/118] WebsocketPagemaker can now access the uweb3 templateparser. WebsocketPageMaker can now access Flash/ExtendTemplate --- uweb3/pagemaker/__init__.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 8fac0e74..d730dfaa 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -216,14 +216,13 @@ def parser(self): self.persistent.Set('__parser', templateparser.Parser( self.options.get('templates', {}).get('path', self.TEMPLATE_DIR))) parser = self.persistent.Get('__parser') - if hasattr(self, "messages"): - parser.messages = self.messages - parser.templates = self.extended_templates - parser.storage = self.storage + parser.messages = self.messages + parser.templates = self.extended_templates + parser.storage = self.storage return parser -class WebsocketPageMaker(Base): +class WebsocketPageMaker(Base, Storage): """Pagemaker for the websocket routes. This is different from the BasePageMaker as we choose to not have a database connection in our WebSocketPageMaker. @@ -231,9 +230,8 @@ class WebsocketPageMaker(Base): This class lacks pretty much all functionality that the BasePageMaker has. """ - #TODO: What functions/methods need to be in this class + #TODO: Add request to pagemaker? def __init__(self): - #request self.persistent = self.PERSISTENT From e2dd5e1330627dddae5eb0313c4ad9694fa4a9f3 Mon Sep 17 00:00:00 2001 From: Stef Houten Date: Mon, 8 Jun 2020 10:19:11 +0200 Subject: [PATCH 035/118] PageMaker can now access request --- uweb3/pagemaker/__init__.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index d730dfaa..05a8b0a1 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -162,11 +162,20 @@ def is_valid(self, supplied_token): token = self.generate_token() return token != supplied_token -class Storage(object): +class Base(object): + # Constant for persistent storage accross requests. This will be accessible + # by all threads of the same application (in the same Python process). + PERSISTENT = CacheStorage() + # Base paths for templates and public data. These are used in the PageMaker + # classmethods that set up paths specific for that pagemaker. + PUBLIC_DIR = 'static' + TEMPLATE_DIR = 'templates' + def __init__(self): self.storage = {} self.messages = [] self.extended_templates = {} + self.persistent = self.PERSISTENT def Flash(self, message): """Appends message to list, list element is vailable in the template under keyword messages @@ -195,15 +204,6 @@ def ExtendTemplate(self, title, template, **kwds): raise ValueError("There is already a template with this title") self.extended_templates[title] = self.parser.Parse(template, **kwds) -class Base(object): - # Constant for persistent storage accross requests. This will be accessible - # by all threads of the same application (in the same Python process). - PERSISTENT = CacheStorage() - # Base paths for templates and public data. These are used in the PageMaker - # classmethods that set up paths specific for that pagemaker. - PUBLIC_DIR = 'static' - TEMPLATE_DIR = 'templates' - @property def parser(self): """Provides a templateparser.Parser instance. @@ -222,7 +222,7 @@ def parser(self): return parser -class WebsocketPageMaker(Base, Storage): +class WebsocketPageMaker(Base): """Pagemaker for the websocket routes. This is different from the BasePageMaker as we choose to not have a database connection in our WebSocketPageMaker. @@ -231,11 +231,14 @@ class WebsocketPageMaker(Base, Storage): """ #TODO: Add request to pagemaker? - def __init__(self): - self.persistent = self.PERSISTENT - + def Connect(self, sid, env): + """This is the connect event, + sets the req variable that contains the request headers. + """ + print(f"User connected with SocketID {sid}: ") + self.req = env -class BasePageMaker(Base, Storage): +class BasePageMaker(Base): """Provides the base pagemaker methods for all the html generators.""" _registery = [] @@ -273,7 +276,6 @@ def __init__(self, self.put = req.vars['put'] self.delete = req.vars['delete'] self.options = config or {} - self.persistent = self.PERSISTENT self.secure_cookie_connection = (self.req, self.cookies, secure_cookie_secret) self.set_invalid_xsrf_token_flag(XSRF_seed) From 9095f96f9a3799e312a9f281399e62daecacecd4 Mon Sep 17 00:00:00 2001 From: Jan Klopper Date: Mon, 15 Jun 2020 16:14:20 +0200 Subject: [PATCH 036/118] Update CONTRIBUTORS --- CONTRIBUTORS | 1 + 1 file changed, 1 insertion(+) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 87031f30..80f0abad 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -3,6 +3,7 @@ uWeb was created by: - Jan Klopper - Arjen Pander - Elmer de Looff +- Stef van Houten And has had code contributions from: From 91afafd96b4b8bbfe7f67402da6e7dbc327c99f9 Mon Sep 17 00:00:00 2001 From: Jan Klopper Date: Mon, 15 Jun 2020 16:15:37 +0200 Subject: [PATCH 037/118] Delete tables.sql remove emtpy file --- tables.sql | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 tables.sql diff --git a/tables.sql b/tables.sql deleted file mode 100644 index e69de29b..00000000 From 1b5831396cce155245a1a0b1757b400f945a33c8 Mon Sep 17 00:00:00 2001 From: Jan Klopper Date: Mon, 15 Jun 2020 16:32:49 +0200 Subject: [PATCH 038/118] html5 compatible error page --- uweb3/pagemaker/http_500.html | 381 ++++++++++++++++++---------------- 1 file changed, 199 insertions(+), 182 deletions(-) diff --git a/uweb3/pagemaker/http_500.html b/uweb3/pagemaker/http_500.html index d7a3a92b..f7ea4925 100644 --- a/uweb3/pagemaker/http_500.html +++ b/uweb3/pagemaker/http_500.html @@ -1,93 +1,94 @@ - - + + - Well, that's embarrassing - + Well, that's embarrassing µWeb3 500 Error. + +

Internal Server Error (HTTP 500)

{{ if [error_for_error] }}

Error page for error page

@@ -97,107 +98,123 @@

Error page for error page

An error occurred on the server during the processing of your request

Here's what we know went wrong, though we still have to figure out why:

{{ endif }} -

[exc:type]

-

[exc:value]

-

Traceback (most recent call first)

-
    - {{ for frame in [exc:traceback] }} -
  1. -
      -
    • File: "[frame:file]"
    • -
    • Scope: [frame:scope]
    • +
+
+
+
+

[exc:type]

+

[exc:value]

+
+

Traceback (most recent call first)

+
    + {{ for frame in [exc:traceback] }}
  1. - - - - {{ for filename, line_no in [frame:source] }} - - {{ endfor }} - -
    Source code
    [filename][line_no]
    +
      +
    • File: "[frame:file]"
    • +
    • Scope: [frame:scope]
    • +
    • + + + + {{ for filename, line_no in [frame:source] }} + + {{ endfor }} + +
      Source code
      [filename][line_no]
      +
    • + {{ if not [error_for_error] }} +
    • + + + + {{ for name, value in [frame:locals|items|sorted] }} + + {{ endfor }} + +
      Frame locals
      [name][value]
      +
    • + {{ endif }} +
  2. - {{ if not [error_for_error] }} -
  3. - - - - {{ for name, value in [frame:locals|items|sorted] }} - - {{ endfor }} - -
    Frame locals
    [name][value]
    -
  4. - {{ endif }} - - - {{ endfor }} -
- {{ if [error_for_error] }} -

Original error (that the error page broke on)

-

[orig_exc:type]

-

[orig_exc:value]

-

Traceback (most recent call first)

-
    - {{ for frame in [orig_exc:traceback] }} -
  1. -
      -
    • File: "[frame:file]"
    • -
    • Scope: [frame:scope]
    • + {{ endfor }} +
+
+ {{ if [error_for_error] }} +
+
+

Original error (that the error page broke on)

+ +

[orig_exc:type]

+

[orig_exc:value]

+
+ +

Traceback (most recent call first)

+
    + {{ for frame in [orig_exc:traceback] }}
  1. - - - - {{ for filename, line_no in [frame:source] }} - - {{ endfor }} - -
    Source code
    [filename][line_no]
    +
      +
    • File: "[frame:file]"
    • +
    • Scope: [frame:scope]
    • +
    • + + + + {{ for filename, line_no in [frame:source] }} + + {{ endfor }} + +
      Source code
      [filename][line_no]
      +
    • +
  2. - - - {{ endfor }} -
- {{ endif }} -

Environment information

- {{ if [cookies] }} - - - - {{ for name, value in [cookies|items|sorted] }} - - {{ endfor }} - -
Cookies
[name][value]
- {{ endif }} - {{ if [query_args] }} - - - - {{ for name, value in [query_args|items|sorted] }} - - {{ endfor }} - -
Query arguments (GET)
[name][value]
- {{ endif }} - {{ if [post_data] }} - - - - {{ for name, value in [post_data|items|sorted] }} - {{ endfor }} - -
POST data
[name][value]
- {{ endif }} - {{ if [environ] }} - - - - {{ for name, value in [environ|items|sorted] }} - - {{ endfor }} - -
Full environment
[name][value]
- {{ endif }} + +
+ {{ endif }} + +
+

Environment information

+ {{ if [cookies] }} + + + + {{ for name, value in [cookies|items|sorted] }} + + {{ endfor }} + +
Cookies
[name][value]
+ {{ endif }} + {{ if [query_args] }} + + + + {{ for name, value in [query_args|items|sorted] }} + + {{ endfor }} + +
Query arguments (GET)
[name][value]
+ {{ endif }} + {{ if [post_data] }} + + + + {{ for name, value in [post_data|items|sorted] }} + + {{ endfor }} + +
POST data
[name][value]
+ {{ endif }} + {{ if [environ] }} + + + + {{ for name, value in [environ|items|sorted] }} + + {{ endfor }} + +
Full environment
[name][value]
+ {{ endif }} +
+
From 6f2f58c54d510abfd867f1a1ac8930e694f4d97f Mon Sep 17 00:00:00 2001 From: Jan Klopper Date: Mon, 15 Jun 2020 16:56:56 +0200 Subject: [PATCH 039/118] clean up and merge decorators --- uweb3/pagemaker/decorators.py | 272 +++++++++++++++--------------- uweb3/pagemaker/new_decorators.py | 38 ----- 2 files changed, 135 insertions(+), 175 deletions(-) delete mode 100644 uweb3/pagemaker/new_decorators.py diff --git a/uweb3/pagemaker/decorators.py b/uweb3/pagemaker/decorators.py index f04e8be5..81adf5cc 100644 --- a/uweb3/pagemaker/decorators.py +++ b/uweb3/pagemaker/decorators.py @@ -1,157 +1,155 @@ """This file holds all the decorators we use in this project.""" -import pickle -import time +import codecs from datetime import datetime - +import hashlib +import pickle import pytz import simplejson -import codecs -# import _mysql_exceptions -import uweb3 -from uweb3 import model +import time + from pymysql import Error +import uweb3 +from uweb3 import model +from uweb3.request import PostDictionary def loggedin(f): - """Decorator that checks if the user requesting the page is logged in.""" - def wrapper(*args, **kwargs): - try: - args[0].user = args[0]._CurrentUser() - except (uweb3.model.NotExistError, args[0].NoSessionError): - path = '/login' - if args[0].req.env['PATH_INFO'].strip() != '': - path = '%s/%s' % (path, args[0].req.env['PATH_INFO'].strip()) - return uweb.Redirect(path) - return f(*args, **kwargs) - return wrapper + """Decorator that checks if the user requesting the page is logged in based on set cookie.""" + def wrapper(*args, **kwargs): + if not args[0].user: + return args[0].req.Redirect('/login', httpcode=303) + return f(*args, **kwargs) + return wrapper def checkxsrf(f): - """Decorator that checks the user's XSRF. + """Decorator that checks the user's XSRF. + The function will compare the XSRF in the user's cookie and in the + (post) request. Make sure to have xsrf_enabled = True in the config.ini + """ + def _clear_form_data(*args): + method = args[0].req.method.lower() + #Set an attribute in the pagemaker that holds the form data on an invalid XSRF validation + args[0].invalid_form_data = getattr(args[0], method) + #Remove the form data from the PageMaker + setattr(args[0], method, PostDictionary()) + #Remove the form data from the Request class + args[0].req.vars[method] = PostDictionary() + return args - The function will compare the XSRF in the user's cookie and in the - (post) request. - """ - def wrapper(*args, **kwargs): - if args[0].incorrect_xsrf_token: - args[0].post.list = [] - return args[0].XSRFInvalidToken( - 'Your XSRF token was incorrect, please try again.') - return f(*args, **kwargs) - return wrapper - -def validapikey(f): - """Decorator that checks if the user requesting the page is using a valid api key.""" - def wrapper(*args, **kwargs): - if not args[0].apikey: - return args[0].NoSessionError('Your API key was incorrect, please try again.') - return f(*args, **kwargs) - return wrapper + def wrapper(*args, **kwargs): + if args[0].req.method != "GET": + if args[0].invalid_xsrf_token: + args = _clear_form_data(*args) + return args[0].XSRFInvalidToken('XSRF token is invalid or missing') + return f(*args, **kwargs) + return wrapper def Cached(maxage=None, verbose=False, handler=None, *t_args, **t_kwargs): - """Decorator that wraps checks the cache table for a cached page. - The function will see if we have a recent cached output for this call, - or if one is being created as we speak. - Use by adding the decorator module and flagging a pagemaker function with - it. - from pages import decorators - @decorators.Cached(60) - def mypage() + """Decorator that wraps checks the cache table for a cached page. + The function will see if we have a recent cached output for this call, + or if one is being created as we speak. + Use by adding the decorator module and flagging a pagemaker function with + it. + from pages import decorators + @decorators.Cached(60) + def mypage() - Arguments: - #TODO: Make handler an argument instead of a kwd since it is required? - @ handler: class CustomClass(model.Record, model.CachedPage) - This is some sort of custom mixin class that we use to store our cached page in the database - % maxage: int(60) - Cache time in seconds. - % verbose: bool(False) - Insert html comment with cache information. - Raises: - KeyError - """ - def cache_decorator(f): - def wrapper(*args, **kwargs): - if not handler: - raise KeyError("A handler is required for storing this page into the database.") - create = False - name = f.__name__ - modulename = f.__module__ - handler.Clean(args[0].connection, maxage) - requesttime = time.time() - time.clock() - sleep = 0.3 - try: # see if we have a cached version thats not too old - data = handler.FromSignature(args[0].connection, - maxage, - name, modulename, - simplejson.dumps(args[1:]), - simplejson.dumps(kwargs)) - if verbose: - data = '%s' % ( - pickle.loads(codecs.decode(data['data'].encode(), "base64")), - data['age']) - else: - data = pickle.loads(codecs.decode(data['data'].encode(), "base64")) - except model.CurrentlyWorking: # we dont have anything fresh enough, but someones working on it - age = 0 - while age < maxage: # as long as there's no output, we should try periodically until we have waited too long - time.sleep(sleep) - age = (time.time() - requesttime) - try: - data = handler.FromSignature(args[0].connection, - maxage, - name, modulename, - simplejson.dumps(args[1:]), - simplejson.dumps(kwargs)) - break - except Exception: - sleep = min(sleep*2, 2) + Arguments: + #TODO: Make handler an argument instead of a kwd since it is required? + @ handler: class CustomClass(model.Record, model.CachedPage) + This is some sort of custom mixin class that we use to store our cached page in the database + % maxage: int(60) + Cache time in seconds. + % verbose: bool(False) + Insert html comment with cache information. + Raises: + KeyError + """ + def cache_decorator(f): + def wrapper(*args, **kwargs): + if not handler: + raise KeyError("A handler is required for storing this page into the database.") + create = False + name = f.__name__ + modulename = f.__module__ + handler.Clean(args[0].connection, maxage) + requesttime = time.time() + time.clock() + sleep = 0.3 + maxsleepinterval = 2 + try: # see if we have a cached version thats not too old + data = handler.FromSignature(args[0].connection, + maxage, + name, modulename, + simplejson.dumps(args[1:]), + simplejson.dumps(kwargs)) + if verbose: + data = '%s' % ( + pickle.loads(codecs.decode(data['data'].encode(), "base64")), + data['age']) + else: + data = pickle.loads(codecs.decode(data['data'].encode(), "base64")) + except model.CurrentlyWorking: # we dont have anything fresh enough, but someones working on it + age = 0 + while age < maxage: # as long as there's no output, we should try periodically until we have waited too long + time.sleep(sleep) + age = (time.time() - requesttime) try: - if verbose: - data = '%s' % ( - pickle.loads(codecs.decode(data['data'].encode(), "base64")), - age) - else: - data = pickle.loads(codecs.decode(data['data'].encode(), "base64")) - except NameError: - create = True - except uweb3.model.NotExistError: # we don't have anything fresh enough, lets create + data = handler.FromSignature(args[0].connection, + maxage, + name, modulename, + simplejson.dumps(args[1:]), + simplejson.dumps(kwargs)) + break + except Exception: + sleep = min(sleep*2, maxsleepinterval) + try: + data = pickle.loads(codecs.decode(data['data'].encode(), "base64")) + if verbose: + data += '' % age + except NameError: create = True - if create: - try: - cache = handler.Create(args[0].connection, { - 'name': name, - 'modulename': modulename, - 'args': simplejson.dumps(args[1:]), - 'kwargs': simplejson.dumps(kwargs), - 'creating': str(pytz.utc.localize(datetime.utcnow()))[0:19], - 'created': str(pytz.utc.localize(datetime.utcnow()))[0:19] - }) - data = f(*args, **kwargs) - cache['data'] = codecs.encode(pickle.dumps(data), "base64").decode() - cache['created'] = str(pytz.utc.localize(datetime.utcnow()))[0:19] - cache['creating'] = None - cache.Save() - if verbose: - data = '%s' % data - except Error: #This is a pymysql Error - pass - return data - return wrapper - return cache_decorator + except uweb3.model.NotExistError: # we don't have anything fresh enough, lets create + create = True + if create: + try: + now = str(pytz.utc.localize(datetime.utcnow()))[0:19] + # create the db row for this call, let other processes know we are working on it. + cache = handler.Create(args[0].connection, { + 'name': name, + 'modulename': modulename, + 'args': simplejson.dumps(args[1:]), + 'kwargs': simplejson.dumps(kwargs), + 'creating': now, + 'created': now + }) + data = f(*args, **kwargs) + cache['data'] = codecs.encode(pickle.dumps(data), "base64").decode() + # update the created time to now, as we are done. + cache['created'] = str(pytz.utc.localize(datetime.utcnow()))[0:19] + cache['creating'] = None + cache.Save() + if verbose: + data += '' + except Error: #This is probably a pymysql Error. or db collision, whilst unfortunate, we wont break the page on this + pass + return data + return wrapper + return cache_decorator def TemplateParser(template, *t_args, **t_kwargs): - """Decorator that wraps and returns the output. + """Decorator that wraps and returns the output. - The output is wrapped in a templateparser call if its not already something - that we prepared for direct output to the client. - """ - def template_decorator(f): - def wrapper(*args, **kwargs): - pageresult = f(*args, **kwargs) or {} - if not isinstance(pageresult, (str, uweb3.Response, uweb3.Redirect)): - pageresult.update(args[0].CommonBlocks(*t_args, **t_kwargs)) - return args[0].parser.Parse(template, **pageresult) - return pageresult - return wrapper - return template_decorator + The output is wrapped in a templateparser call if its not already something + that we prepared for direct output to the client. + """ + def template_decorator(f): + def wrapper(*args, **kwargs): + pageresult = f(*args, **kwargs) or {} + if not isinstance(pageresult, (str, uweb3.Response, uweb3.Redirect)): + pageresult.update(args[0].CommonBlocks(*t_args, **t_kwargs)) + return args[0].parser.Parse(template, **pageresult) + return pageresult + return wrapper + return template_decorator diff --git a/uweb3/pagemaker/new_decorators.py b/uweb3/pagemaker/new_decorators.py deleted file mode 100644 index 92a766b5..00000000 --- a/uweb3/pagemaker/new_decorators.py +++ /dev/null @@ -1,38 +0,0 @@ -import os -import time -import datetime -import hashlib -from uweb3.request import PostDictionary - -def loggedin(f): - """Decorator that checks if the user requesting the page is logged in based on set cookie.""" - def wrapper(*args, **kwargs): - if not args[0].user: - return args[0].req.Redirect('/login', httpcode=303) - return f(*args, **kwargs) - return wrapper - -def clear_form_data(*args): - method = args[0].req.method.lower() - #Set an attribute in the pagemaker that holds the form data on an invalid XSRF validation - args[0].invalid_form_data = getattr(args[0], method) - #Remove the form data from the PageMaker - setattr(args[0], method, PostDictionary()) - #Remove the form data from the Request class - args[0].req.vars[method] = PostDictionary() - return args - -def checkxsrf(f): - """Decorator that checks the user's XSRF. - - The function will compare the XSRF in the user's cookie and in the - (post) request. Make sure to have xsrf_enabled = True in the config.ini - """ - def wrapper(*args, **kwargs): - if args[0].req.method != "GET": - if args[0].invalid_xsrf_token: - args = clear_form_data(*args) - return args[0].XSRFInvalidToken('XSRF token is invalid or missing') - return f(*args, **kwargs) - return wrapper - From b468ca9783426534f043a1b8b1cbcdde3cecdd2d Mon Sep 17 00:00:00 2001 From: Jan Klopper Date: Mon, 26 Oct 2020 17:26:42 +0100 Subject: [PATCH 040/118] Rework this repos to the current state of development --- DEVELOPMENT | 23 + README.md | 9 +- setup.py | 14 +- uweb3/__init__.py | 278 ++++--- uweb3/connections.py | 318 +++++++ uweb3/ext_lib/libs/__init__.py | 0 uweb3/ext_lib/libs/sqltalk/mysql/__init__.py | 56 -- uweb3/ext_lib/libs/sqltalk/mysql/constants.py | 598 -------------- .../ext_lib/libs/sqltalk/mysql/converters.py | 217 ----- uweb3/ext_lib/libs/sqltalk/mysql/times.py | 133 --- uweb3/ext_lib/libs/sqltalk/sqlite/__init__.py | 33 - uweb3/helpers.py | 137 ---- uweb3/{ext_lib => libs}/__init__.py | 0 uweb3/libs/mail.py | 156 ++++ .../{ext_lib => }/libs/safestring/__init__.py | 104 ++- uweb3/{ext_lib => }/libs/safestring/test.py | 0 uweb3/{ext_lib => }/libs/sqltalk/__init__.py | 4 +- uweb3/libs/sqltalk/mysql/__init__.py | 17 + .../libs/sqltalk/mysql/connection.py | 67 +- .../libs/sqltalk/mysql/cursor.py | 17 +- uweb3/libs/sqltalk/sqlite/__init__.py | 33 + .../libs/sqltalk/sqlite/connection.py | 54 +- .../libs/sqltalk/sqlite/converters.py | 12 +- .../libs/sqltalk/sqlite/cursor.py | 15 +- uweb3/{ext_lib => }/libs/sqltalk/sqlresult.py | 28 +- .../libs/sqltalk/sqlresult_test.py | 0 .../libs/urlsplitter/__init__.py | 0 uweb3/{ext_lib => }/libs/urlsplitter/test.py | 0 uweb3/libs/utils.py | 774 ++++++++++++++++++ uweb3/model.py | 634 ++++++++------ uweb3/pagemaker/__init__.py | 499 ++++++----- uweb3/pagemaker/decorators.py | 55 +- uweb3/pagemaker/http_403.html | 27 + uweb3/pagemaker/http_500.html | 2 +- uweb3/request.py | 146 ++-- uweb3/response.py | 16 +- uweb3/templateparser.py | 252 ++++-- uweb3/test_model.py | 0 uweb3/test_model_alchemy.py | 0 uweb3/test_request.py | 6 +- uweb3/test_templateparser.py | 130 ++- 41 files changed, 2711 insertions(+), 2153 deletions(-) create mode 100644 DEVELOPMENT create mode 100644 uweb3/connections.py delete mode 100644 uweb3/ext_lib/libs/__init__.py delete mode 100644 uweb3/ext_lib/libs/sqltalk/mysql/__init__.py delete mode 100644 uweb3/ext_lib/libs/sqltalk/mysql/constants.py delete mode 100644 uweb3/ext_lib/libs/sqltalk/mysql/converters.py delete mode 100644 uweb3/ext_lib/libs/sqltalk/mysql/times.py delete mode 100644 uweb3/ext_lib/libs/sqltalk/sqlite/__init__.py delete mode 100644 uweb3/helpers.py rename uweb3/{ext_lib => libs}/__init__.py (100%) create mode 100644 uweb3/libs/mail.py rename uweb3/{ext_lib => }/libs/safestring/__init__.py (79%) rename uweb3/{ext_lib => }/libs/safestring/test.py (100%) rename uweb3/{ext_lib => }/libs/sqltalk/__init__.py (80%) create mode 100644 uweb3/libs/sqltalk/mysql/__init__.py rename uweb3/{ext_lib => }/libs/sqltalk/mysql/connection.py (89%) rename uweb3/{ext_lib => }/libs/sqltalk/mysql/cursor.py (97%) create mode 100644 uweb3/libs/sqltalk/sqlite/__init__.py rename uweb3/{ext_lib => }/libs/sqltalk/sqlite/connection.py (84%) rename uweb3/{ext_lib => }/libs/sqltalk/sqlite/converters.py (91%) rename uweb3/{ext_lib => }/libs/sqltalk/sqlite/cursor.py (93%) rename uweb3/{ext_lib => }/libs/sqltalk/sqlresult.py (92%) rename uweb3/{ext_lib => }/libs/sqltalk/sqlresult_test.py (100%) rename uweb3/{ext_lib => }/libs/urlsplitter/__init__.py (100%) rename uweb3/{ext_lib => }/libs/urlsplitter/test.py (100%) create mode 100644 uweb3/libs/utils.py create mode 100644 uweb3/pagemaker/http_403.html mode change 100644 => 100755 uweb3/test_model.py mode change 100644 => 100755 uweb3/test_model_alchemy.py mode change 100644 => 100755 uweb3/test_request.py mode change 100644 => 100755 uweb3/test_templateparser.py diff --git a/DEVELOPMENT b/DEVELOPMENT new file mode 100644 index 00000000..3461ce5f --- /dev/null +++ b/DEVELOPMENT @@ -0,0 +1,23 @@ +Development on µWeb3 follows the pep8 rules + +# Development environment. +You can setup a working development environment by issueing: + +```bash +python3 setup.py develop --user +cd uweb3/scaffold + +python3 serve.py +``` + +This will setup a local + + +# Coding conventions: + +* Tabs are two spaces. +* Each method and class is required to contain a docstring +* Text files outside the python scope are written in markdown + +Each File has an __author__ variable, in which you can list your name and email address if you whish to do so. +You can do a pull request on CONTRIBUTORS to have your name added. diff --git a/README.md b/README.md index 6f9e1511..eb14d7a0 100644 --- a/README.md +++ b/README.md @@ -31,7 +31,7 @@ source env/bin/activate python3 setup.py install # Or you can install in development mode which allows easy modification of the source: -python3 setup.py develop +python3 setup.py develop --user cd uweb3/scaffold @@ -82,7 +82,7 @@ The default way to create new routes in µWeb3 is to create a folder called rout In the routes folder create your pagemaker class of choice, the name doesn't matter as long as it inherits from PageMaker. After creating your pagemaker be sure to add the route endpoint to routes list in base/__init__.py. -# New +# New since v3 - In uweb3 __init__ a class called HotReload - In pagemaker __init__: - A classmethod called loadModules that loads all pagemaker modules inheriting from PageMaker class @@ -96,11 +96,10 @@ After creating your pagemaker be sure to add the route endpoint to routes list i - Method called DeleteCookie - A if statement that checks string like cookies and raises an error if the size is equal or bigger than 4096 bytes. - AddCookie method, edited this and the response class to handle the setting of multiple cookies. Previously setting multiple cookies with the Set-Cookie header would make the last cookie the only cookie. -- In pagemaker/new_decorators: +- In pagemaker/decorators: - Loggedin decorator that validates if user is loggedin based on cookie with userid - Checkxsrf decorator that checks if the incorrect_xsrf_token flag is set - In templatepaser: - A function called _TemplateConstructXsrf that generates a hidden input field with the supplied value: {{ xsrf [xsrf_variable]}} - In libs/sqltalk - - Tried to make sqltalk python3 compatible by removing references to: long, unicode and basestring - - So far so good but it might crash on functions that I didn't use yet \ No newline at end of file + - So far so good but it might crash on functions that I didn't use yet diff --git a/setup.py b/setup.py index 35b87f64..69384f84 100644 --- a/setup.py +++ b/setup.py @@ -10,12 +10,12 @@ 'python-magic', 'pytz', 'simplejson', - 'sqlalchemy', - 'bcrypt', - 'werkzeug', - 'mysqlclient', + 'bcrypt' ] +# 'sqlalchemy', +# 'werkzeug', + def description(): with open(os.path.join(os.path.dirname(__file__), 'README.md')) as r_file: return r_file.read() @@ -31,8 +31,8 @@ def version(): name='uWeb3 test', version=version(), description='uWeb, python3, uswgi compatible micro web platform', - long_description=description(), - long_description_content_type='text/markdown', + long_description_file = 'README.md', + long_description_content_type = 'text/markdown', license='ISC', classifiers=[ 'Development Status :: 3 - Alpha', @@ -47,4 +47,4 @@ def version(): packages=find_packages(), include_package_data=True, zip_safe=False, - install_requires=REQUIREMENTS) \ No newline at end of file + install_requires=REQUIREMENTS) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 1b7309d1..b35f17f9 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -1,62 +1,42 @@ -#!/usr/bin/python -"""uWeb3 Framework""" +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +"""µWeb3 Framework""" -__version__ = '0.4.4-dev' +__version__ = '3.0' # Standard modules -try: - import ConfigParser as configparser -except ImportError: - import configparser +import configparser import logging import os import re import sys -import time -import threading from wsgiref.simple_server import make_server -import socket, errno import datetime -# Add the ext_lib directory to the path -sys.path.append( - os.path.abspath(os.path.join(os.path.dirname(__file__), 'ext_lib'))) - # Package modules -from . import pagemaker -from . import request +from . import pagemaker, request # Package classes -from .response import Response -from .pagemaker import PageMaker -from .pagemaker import WebsocketPageMaker -from .pagemaker import DebuggingPageMaker -from .pagemaker import SqAlchemyPageMaker -from .helpers import StaticMiddleware -from uweb3.model import SettingsManager - - -def return_real_remote_addr(env): - """Returns the client addres, - if there is a proxy involved it will take the last IP addres from the HTTP_X_FORWARDED_FOR list - """ - try: - return env['HTTP_X_FORWARDED_FOR'].split(',')[-1].strip() - except KeyError: - return env['REMOTE_ADDR'] +from .response import Response, Redirect +from .pagemaker import PageMaker, decorators, WebsocketPageMaker, DebuggingPageMaker, LoginMixin +from .model import SettingsManager +from .libs.safestring import HTMLsafestring, JSONsafestring, JsonEncoder, Basesafestring class Error(Exception): """Superclass used for inheritance and external exception handling.""" - class ImmediateResponse(Exception): """Used to trigger an immediate response, foregoing the regular returns.""" +class HTTPException(Error): + """SuperClass for HTTP exceptions.""" + +class HTTPRequestException(HTTPException): + """Exception for http request errors.""" class NoRouteError(Error): """The server does not know how to route this request""" - class Registry(object): """Something to hook stuff to""" @@ -83,32 +63,33 @@ def router(self, routes): request_router: Configured closure that processes urls. """ req_routes = [] - #Variable used to store websocket pagemakers, - #these pagemakers are only created at startup but can have multiple routes. - #To prevent creating the same instance for each route we store them in a dict + # Variable used to store websocket pagemakers, + # these pagemakers are only created at startup but can have multiple routes. + # To prevent creating the same instance for each route we store them in a dict websocket_pagemaker = {} for pattern, *details in routes: - pagemaker = None + page_maker = None for pm in self.pagemakers: - #Check if the pagemaker has the method/handler we are looking for + # Check if the page_maker has the method/handler we are looking for if hasattr(pm, details[0]): - pagemaker = pm + page_maker = pm break if callable(pattern): - #Check if the pagemaker is already in the dict, if not instantiate - #if so just use that one. This prevents creating multiple instances for one route. - if not websocket_pagemaker.get(pagemaker.__name__): - websocket_pagemaker[pagemaker.__name__] = pagemaker() - pattern(getattr(websocket_pagemaker[pagemaker.__name__], details[0])) + # Check if the page_maker is already in the dict, if not instantiate + # if so just use that one. This prevents creating multiple instances for one route. + if not websocket_pagemaker.get(page_maker.__name__): + websocket_pagemaker[page_maker.__name__] = page_maker() + pattern(getattr(websocket_pagemaker[page_maker.__name__], details[0])) continue - if not pagemaker: - raise NoRouteError(f"µWeb3 could not find a route handler called '{details[0]}' in any of your projects PageMakers.") + if not page_maker: + raise NoRouteError(f"µWeb3 could not find a route handler called '{details[0]}' in any of the PageMakers, your application will not start.") req_routes.append((re.compile(pattern + '$', re.UNICODE), details[0], #handler, - details[1] if len(details) > 1 else 'ALL', #request types - details[2] if len(details) > 2 else '*', #host - pagemaker #pagemaker + details[1].upper() if len(details) > 1 else 'ALL', #request types + details[2].lower() if len(details) > 2 else '*', #host + page_maker #pagemaker class )) + def request_router(url, method, host): """Returns the appropriate handler and arguments for the given `url`. @@ -135,7 +116,7 @@ def request_router(url, method, host): 2-tuple: handler method (unbound), and tuple of pattern matches. """ - for pattern, handler, routemethod, hostpattern, pagemaker in req_routes: + for pattern, handler, routemethod, hostpattern, page_maker in req_routes: if routemethod != 'ALL': # clearly not the route we where looking for if isinstance(routemethod, tuple): @@ -154,10 +135,14 @@ def request_router(url, method, host): hostmatch = hostmatch.groups() match = pattern.match(url) if match: - return handler, match.groups(), hostmatch, pagemaker + # strip out optional groups, as they return '', which would override + # the handlers default argument values later on in the page_maker + groups = (group for group in match.groups() if group) + return handler, groups, hostmatch, page_maker raise NoRouteError(url +' cannot be handled') return request_router + class uWeb(object): """Returns a configured closure for handling page requests. @@ -182,17 +167,18 @@ class uWeb(object): RequestHandler: Configured closure that is ready to process requests. """ def __init__(self, page_class, routes, executing_path=None): - self.executing_path = executing_path - self.config = SettingsManager(filename='config', executing_path=executing_path) + self.executing_path = executing_path if executing_path else os.path.dirname(__file__) + self.config = SettingsManager(filename='config', executing_path=self.executing_path) self.logger = self.setup_logger() - self.page_class = page_class + self.inital_pagemaker = page_class self.registry = Registry() self.registry.logger = logging.getLogger('root') self.router = Router(page_class).router(routes) self.setup_routing() - #generating random seeds on uWeb3 startup - self.secure_cookie_secret = str(os.urandom(32)) - self.XSRF_seed = str(os.urandom(32)) + self.encoders = { + 'text/html': lambda x: HTMLsafestring(x, unsafe=True), + 'text/plain': str, + 'application/json': lambda x: JSONsafestring(x, unsafe=True)} def __call__(self, env, start_response): """WSGI request handler. @@ -200,51 +186,76 @@ def __call__(self, env, start_response): response and returns a response iterator. """ req = request.Request(env, self.registry) - req.env['REAL_REMOTE_ADDR'] = return_real_remote_addr(req.env) + req.env['REAL_REMOTE_ADDR'] = request.return_real_remote_addr(req.env) + response = None + method = '_NotFound' + args = None + rollback = False try: - method, args, hostargs, pagemaker = self.router(req.path, + method, args, hostargs, page_maker = self.router(req.path, req.env['REQUEST_METHOD'], - req.env['host'] - ) - pagemaker = pagemaker(req, - config=self.config.options, - secure_cookie_secret=self.secure_cookie_secret, - executing_path=self.executing_path, - XSRF_seed=self.XSRF_seed) - - if hasattr(pagemaker, '_PreRequest'): - pagemaker = pagemaker._PreRequest() - - response = self.get_response(pagemaker, method, args) + req.env['host']) except NoRouteError: - #When we catch this error this means there is no method for the expected function - #If this happens we default to the standard pagemaker because we don't know what the target pagemaker should be. - #Then we set an internalservererror and move on - pagemaker = self.page_class(req, - config=self.config.options, - secure_cookie_secret=self.secure_cookie_secret, - executing_path=self.executing_path, - XSRF_seed=self.XSRF_seed) - response = pagemaker.InternalServerError(*sys.exc_info()) + # When we catch this error this means there is no method for the route in the currently selected pagemaker. + # If this happens we default to the initial pagemaker because we don't know what the target pagemaker should be. + # Then we set an internalservererror and move on + page_maker = self.inital_pagemaker + try: + # instantiate the pagemaker for this request + pagemaker_instance = page_maker(req, + config=self.config, + executing_path=self.executing_path) + # specifically call _PreRequest as promised in documentation + if hasattr(pagemaker_instance, '_PreRequest'): + pagemaker_instance = pagemaker_instance._PreRequest() + + response = self.get_response(pagemaker_instance, method, args) except Exception: - #This should only happend when something is very wrong - pagemaker = PageMaker(req, - config=self.config.options, - secure_cookie_secret=self.secure_cookie_secret, - executing_path=self.executing_path, - XSRF_seed=self.XSRF_seed) - response = pagemaker.InternalServerError(*sys.exc_info()) - - if not isinstance(response, Response): - req.response.text = response - response = req.response - - if hasattr(pagemaker, '_PostRequest'): - response = pagemaker._PostRequest(response) + # something broke in our pagemaker_instance, lets fall back to the most basic pagemaker for error output + if hasattr(pagemaker_instance, '_ConnectionRollback'): + try: + pagemaker_instance._ConnectionRollback() + except: + pass + pagemaker_instance = PageMaker(req, + config=self.config, + executing_path=self.executing_path) + response = pagemaker_instance.InternalServerError(*sys.exc_info()) + + if method != 'Static': + if not isinstance(response, Response): + # print('Upgrade response to Response class: %s' % type(response)) + req.response.text = response + response = req.response + + if not isinstance(response.text, Basesafestring): + # make sure we always output Safe HTML if our content type is something we should encode + encoder = self.encoders.get(response.clean_content_type(), None) + if encoder: + response.text = encoder(response.text) + + if hasattr(pagemaker_instance, '_PostRequest'): + pagemaker_instance._PostRequest() + + # CSP might be unneeded for some static content, + # https://github.com/w3c/webappsec/issues/520 + if hasattr(pagemaker_instance, '_CSPheaders'): + pagemaker_instance._CSPheaders() + + # provide users with a _PostRequest method to overide too + if method != 'Static' and hasattr(pagemaker_instance, 'PostRequest'): + response = pagemaker_instance.PostRequest(response) + + # we should at least send out something to make sure we are wsgi compliant. + if not response.text: + response.text = '' self._logging(req, response) start_response(response.status, response.headerlist) - yield response.content.encode(response.charset) + try: + yield response.text.encode(response.charset) + except AttributeError: + yield response.text def setup_logger(self): logger = logging.getLogger('uweb3_logger') @@ -272,18 +283,23 @@ def _logging(self, req, response): def get_response(self, page_maker, method, args): try: - # We're specifically calling _PostInit here as promised in documentation. - # pylint: disable=W0212 - page_maker._PostInit() + if method != 'Static': + # We're specifically calling _PostInit here as promised in documentation. + # pylint: disable=W0212 + page_maker._PostInit() + elif hasattr(page_maker, '_StaticPostInit'): + # We're specifically calling _StaticPostInit here as promised in documentation, seperate from the regular PostInit to keep things fast for static pages + page_maker._StaticPostInit() + # pylint: enable=W0212 return getattr(page_maker, method)(*args) except pagemaker.ReloadModules as message: - reload_message = reload(sys.modules[self.page_class.__module__]) + reload_message = reload(sys.modules[self.inital_pagemaker.__module__]) return Response(content='%s\n%s' % (message, reload_message)) except ImmediateResponse as err: return err[0] except Exception: - if self.config.options.get('development', None): + if self.config.options.get('development', False): if self.config.options['development'].get('error_logging', True) == 'True': logger = logging.getLogger('uweb3_exception_logger') fh = logging.FileHandler(os.path.join(self.executing_path, 'uweb3_uncaught_exceptions.log')) @@ -293,27 +309,33 @@ def get_response(self, page_maker, method, args): def serve(self, hot_reloading=True): """Sets up and starts WSGI development server for the current app.""" - host = self.config.options['development'].get('host', 'localhost') - port = self.config.options['development'].get('port', 8001) - static_directory = [os.path.join(sys.path[0], os.path.join(self.executing_path, 'static'))] - app = StaticMiddleware(self, static_root='static', static_dirs=static_directory) - server = make_server(host, int(port), app) - + host = 'localhost' + port = 8001 + hotreload = False + dev = False + if self.config.options.get('development', False): + host = self.config.options['development'].get('host', host) + port = self.config.options['development'].get('port', port) + hotreload = self.config.options['development'].get('reload', False) == 'True' + dev = self.config.options['development'].get('dev', False) + server = make_server(host, int(port), self) print(f'Running µWeb3 server on http://{server.server_address[0]}:{server.server_address[1]}') + print(f'Root dir is: {self.executing_path}') try: - if self.config.options['development'].get('dev', False) == 'True': - HotReload(self.executing_path, uweb_dev=self.config.options['development'].get('uweb_dev', 'False')) + if hotreload: + print(f'Hot reload is enabled for changes in: {self.executing_path}') + HotReload(self.executing_path, uweb_dev=dev) server.serve_forever() except: server.shutdown() def setup_routing(self): - if isinstance(self.page_class, list): + if isinstance(self.inital_pagemaker, list): routes = [] - for route in self.page_class[1:]: + for route in self.inital_pagemaker[1:]: routes.append(route) - self.page_class[0].AddRoutes(tuple(routes)) - self.page_class = self.page_class[0] + self.inital_pagemaker[0].AddRoutes(tuple(routes)) + self.inital_pagemaker = self.inital_pagemaker[0] default_route = "routes" automatic_detection = True @@ -322,24 +344,24 @@ def setup_routing(self): automatic_detection = self.config.options['routing'].get('disable_automatic_route_detection', 'False') != 'True' if automatic_detection: - self.page_class.LoadModules(default_routes=default_route) - -def read_config(config_file): - """Parses the given `config_file` and returns it as a nested dictionary.""" - parser = configparser.SafeConfigParser() - try: - parser.read(config_file) - except configparser.ParsingError: - raise ValueError('Not a valid config file: %r.' % config_file) - return dict((section, dict(parser.items(section))) - for section in parser.sections()) + self.inital_pagemaker.LoadModules(routes=default_route) + class HotReload(object): + """This class handles the thread which scans for file changes in the + execution path and restarts the server if needed""" + IGNOREDEXTENSIONS = (".pyc", '.ini', '.md', '.html', '.log') + def __init__(self, path, interval=1, uweb_dev=False): + """Takes a path, an optional interval in seconds and an optional flag + signalling a development environment""" + import threading + import time + self.running = threading.Event() self.interval = interval self.path = os.path.dirname(path) - if uweb_dev == 'True': + if uweb_dev in ('True', True): from pathlib import Path self.path = str(Path(self.path).parents[1]) self.thread = threading.Thread(target=self.run, args=()) @@ -380,11 +402,11 @@ def getListOfFiles(self): for r, d, f in os.walk(self.path): for file in f: ext = os.path.splitext(file)[1] - if ext not in (".pyc", '.ini', '.md', '.html', '.log'): + if ext not in IGNOREDEXTENSIONS: watched_files.append(os.path.join(r, file)) return (len(watched_files), watched_files) def restart(self): """Restart uweb3 with all provided system arguments.""" self.running.clear() - os.execl(sys.executable, sys.executable, * sys.argv) \ No newline at end of file + os.execl(sys.executable, sys.executable, * sys.argv) diff --git a/uweb3/connections.py b/uweb3/connections.py new file mode 100644 index 00000000..b42465ce --- /dev/null +++ b/uweb3/connections.py @@ -0,0 +1,318 @@ +#!/usr/bin/python +"""This file contains all the connectionManager classes that interact with +databases, restfull apis, secure cookies, config files etc.""" +__author__ = 'Jan Klopper (janunderdark.nl)' +__version__ = 0.1 + +import os +import sys +from base64 import b64encode + +class ConnectionError(Exception): + """Error class thrown when the underlying connectors thrown an error on + connecting.""" + +class ConnectionManager(object): + """This is the connection manager object that is handled by all Model Objects. + It finds out which connection was requested by looking at the call stack, and + figuring out what database type the model class calling it belongs to. + + Connected databases are stored and reused. + On delete, the databases are closed and any lingering transactions are + committed. to complete the database writes. + """ + + DEFAULTCONNECTIONMANAGER = None + + def __init__(self, config, options, debug): + self.__connectors = {} # classes + self.__connections = {} # instances + self.config = config + self.options = options + self.debug = debug + self.LoadDefaultConnectors() + + def LoadDefaultConnectors(self): + self.RegisterConnector(SignedCookie) + self.RegisterConnector(Mysql, True) + self.RegisterConnector(Sqlite) + self.RegisterConnector(Mongo) + self.RegisterConnector(SqlAlchemy) + + def RegisterConnector(self, classname, default=False): + """Make the ConnectonManager aware of a new type of connector.""" + if default: + self.DEFAULTCONNECTIONMANAGER = classname.Name() + self.__connectors[classname.Name()] = classname + + def RelevantConnection(self, level=2): + """Returns the relevant database connection dependant on the caller model + class. + + If the caller model cannot be determined, the 'relational' database + connection is returned as a fallback method. + + Level indicates how many stack layers we should go up. Defaults to two. + """ + # Figure out caller type or instance + # pylint: disable=W0212 + #TODO use inspect module instead, and iterate over frames + caller_locals = sys._getframe(level).f_locals + # pylint: enable=W0212 + if 'self' in caller_locals: + caller_cls = type(caller_locals['self']) + else: + caller_cls = caller_locals.get('cls', type) + + # Decide the type of connection to return for this caller + con_type = (caller_cls._CONNECTOR if hasattr(caller_cls, '_CONNECTOR') else + self.DEFAULTCONNECTIONMANAGER) + if (con_type in self.__connections and + hasattr(self.__connections[con_type], 'connection')): + return self.__connections[con_type].connection + else: + request = sys._getframe(3).f_locals['self'].req + try: + # instantiate a connection + self.__connections[con_type] = self.__connectors[con_type]( + self.config, self.options, request, self.debug) + return self.__connections[con_type].connection + except KeyError: + raise TypeError('No connector for: %r, available: %r' % (con_type, self.__connectors)) + + def __enter__(self): + """Proxies the transaction to the underlying relevant connection.""" + return self.RelevantConnection().__enter__() + + def __exit__(self, *args): + """Proxies the transaction to the underlying relevant connection.""" + return self.RelevantConnection().__exit__(*args) + + def __getattr__(self, attribute): + return getattr(self.RelevantConnection(), attribute) + + def RollbackAll(self): + """Performas a rollback on all connectors with pending commits""" + if self.debug: + print('Rolling back uncommited transaction on all connectors.') + for classname in self.__connections: + try: + self.__connections[classname].Rollback() + except NotImplementedError: + pass + + def PostRequest(self): + """This cleans up any non persistent connections.""" + cleanups = [] + for classname in self.__connections: + if (hasattr(self.__connections[classname], 'PERSISTENT') and + not self.__connections[classname].PERSISTENT): + cleanups.append(classname) + for classname in cleanups: + try: + self.__connections[classname].Disconnect() + except (NotImplementedError, TypeError, ConnectionError): + pass + del(self.__connections[classname]) + + def __iter__(self): + """Pass tru to the Relevant connection as an Iterable, so variable unpacking + can be used by the consuming class. This is used in the SecureCookie Model + class.""" + return iter(self.RelevantConnection()) + + def __del__(self): + """Cleans up all references, and closes all connectors""" + print('Deleting model connections.') + for classname in self.__connectors: + if not hasattr(self.__connectors[classname], 'connection'): + continue + try: + self.__connections[classname].Disconnect() + except (NotImplementedError, TypeError, ConnectionError): + pass + + +class Connector(object): + """Base Connector class, subclass from this to create your own connectors. + Usually the name of your class is used to lookup its config in the + configuration file, or the database or local filename. + + Connectors based on this class are Usually Singletons. One global connection + is kept alive, and multiple model classes use it to connect to their + respective tables, cookies, or files. + """ + _NAME = None + + @classmethod + def Name(cls): + """Returns the 'connector' name, which is usally used to lookup its config + in the config file. + + If this is not explicitly defined by the class constant `_TABLE`, the return + value will be the class name with the first letter lowercased. + """ + if cls._NAME: + return cls._NAME + name = cls.__name__ + return name[0].lower() + name[1:] + + def Disconnect(self): + """Standard interface to disconnect from data source""" + raise NotImplementedError + + def Rollback(self): + """Standard interface to rollback any pending commits""" + raise NotImplementedError + + +class SignedCookie(Connector): + """Adds a signed cookie connection to the connection manager object. + + The name of the class is used as the Cookiename""" + + PERSISTENT = False + + def __init__(self, config, options, request, debug=False): + """Sets up the local connection to the signed cookie store, and generates a + new secret key if no key can be found in the config""" + # Generating random seeds on uWeb3 startup or fetch from config + self.debug = debug + try: + self.options = options[self.Name()] + self.secure_cookie_secret = self.options['secret'] + except KeyError: + secret = self.GenerateNewKey() + config.Create(self.Name(), 'secret', secret) + if self.debug: + print('SignedCookie: Wrote new secret random to config.') + self.secure_cookie_secret = secret + self.connection = (request, request.vars['cookie'], self.secure_cookie_secret) + + @staticmethod + def GenerateNewKey(length=128): + return b64encode(os.urandom(length)).decode('utf-8') + + +class Mysql(Connector): + """Adds MySQL support to connection manager object.""" + + def __init__(self, config, options, request, debug=False): + """Returns a MySQL database connection.""" + self.debug = debug + self.options = {'host': 'localhost', + 'user': None, + 'password': None, + 'database': ''} + try: + from .libs.sqltalk import mysql + try: + self.options = options[self.Name()] + except KeyError: + pass + self.connection = mysql.Connect( + host=self.options.get('host', 'localhost'), + user=self.options.get('user'), + passwd=self.options.get('password'), + db=self.options.get('database'), + charset=self.options.get('charset', 'utf8'), + debug=self.debug) + except Exception as e: + raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) + + def Rollback(self): + with self.connection as cursor: + return cursor.Execute("ROLLBACK") + + def Disconnect(self): + """Closes the MySQL connection.""" + if self.debug: + print('%s closed connection to: %r' % (self.Name(), self.options.get('database'))) + self.connection.close() + del(self.connection) + + +class Mongo(Connector): + """Adds MongoDB support to connection manager object.""" + + def __init__(self, config, options, request, debug=False): + """Returns a MongoDB database connection.""" + self.debug = debug + import pymongo + self.options = options.get(self.Name(), {}) + try: + self.connection = pymongo.connection.Connection( + host=self.options.get('host', 'localhost'), + port=self.options.get('port', 27017)) + if 'database' in self.options: + self.connection = self.connection[self.options['database']] + except Exception as e: + raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) + + def Disconnect(self): + """Closes the Mongo connection.""" + if self.debug: + print('%s closed connection to: %r' % (self.Name(), self.options.get('database', 'Unspecified'))) + self.connection.close() + del(self.connection) + + +class SqlAlchemy(Connector): + """Adds MysqlAlchemy connection to ConnectionManager.""" + + def __init__(self, config, options, request, debug=False): + """Returns a Mysql database connection wrapped in a SQLAlchemy session.""" + from sqlalchemy.orm import sessionmaker + self.debug = debug + self.options = {'host': 'localhost', + 'user': None, + 'password': None, + 'database': ''} + try: + self.options = options[self.Name()] + except KeyError: + pass + Session = sessionmaker() + Session.configure(bind=self.engine, expire_on_commit=False) + try: + self.connection = Session() + except Exception as e: + raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) + + def engine(self): + from sqlalchemy import create_engine + return create_engine('mysql://{username}:{password}@{host}/{database}'.format( + username=self.options.get('user'), + password=self.options.get('password'), + host=self.options.get('host', 'localhost'), + database=self.options.get('database')), + pool_size=5, + max_overflow=0, + encoding=self.options.get('charset', 'utf8'),) + + +class Sqlite(Connector): + """Adds SQLite support to connection manager object.""" + + def __init__(self, config, options, request, debug=False): + """Returns a SQLite database connection. + The name of the class is used as the local filename. + """ + from .libs.sqltalk import sqlite + self.debug = debug + self.options = options[self.Name()] + try: + self.connection = sqlite.Connect(self.options.get('database')) + except Exception as e: + raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) + + def Rollback(self): + """Rolls back any uncommited transactions.""" + return self.connection.rollback() + + def Disconnect(self): + """Closes the SQLite connection.""" + if self.debug: + print('%s closed connection to: %r' % (self.Name(), self.options.get('database'))) + self.connection.close() + del(self.connection) diff --git a/uweb3/ext_lib/libs/__init__.py b/uweb3/ext_lib/libs/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/uweb3/ext_lib/libs/sqltalk/mysql/__init__.py b/uweb3/ext_lib/libs/sqltalk/mysql/__init__.py deleted file mode 100644 index 97e2028c..00000000 --- a/uweb3/ext_lib/libs/sqltalk/mysql/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/python2.5 -"""SQLTalk MySQL interface package. - -This package provides a full SQLTalk interface against a MySQL database. -Required MySQL version is unknown but expected to be 4, however, migrating to -MySQL version 5 is in everyone's best interest :). - -This package is built on a heavily modified version of MySQLdb 1.2.2 which was -originally written by Andy Dustman 4.1 returns TIMESTAMP in the same format as DATETIME - if len(stamp) == 19: - return DateTimeOrNone(stamp) - try: - stamp = stamp.ljust(14, '0') - return INTERPRET_AS_UTC(datetime.datetime( - *map(int, (stamp[:4], stamp[4:6], stamp[6:8], - stamp[8:10], stamp[10:12], stamp[12:14])))) - except ValueError: - return None - - -def TimeDeltaOrNone(string): - """Converts an input string to a datetime.timedelta object. - - Returns: - datetime.timedelta object, or None if input is bad. - """ - try: - hour, minute, second = map(float, string.split(':')) - return (-1, 1)[hour > 0] * datetime.timedelta( - hours=hour, minutes=minute, seconds=second) - except ValueError: - return None - - -def TimeFromTicks(ticks): - """Convert UNIX ticks into a time instance.""" - return INTERPRET_AS_UTC(datetime.time(*time.gmtime(ticks)[3:6])) - - -def TimeOrNone(string): - """Converts an input string to a datetime.time object. - - Returns: - datetime.time object, or None if input is bad. - """ - try: - hour, minute, second = string.split(':') - micro, second = math.modf(float(second)) - return INTERPRET_AS_UTC(datetime.time( - hour=int(hour), minute=int(minute), - second=int(second), microsecond=int(micro * 1000000))) - except ValueError: - return None - - -def TimestampFromTicks(ticks): - """Convert UNIX ticks into a datetime instance.""" - return INTERPRET_AS_UTC(datetime.datetime.utcfromtimestamp(ticks)) diff --git a/uweb3/ext_lib/libs/sqltalk/sqlite/__init__.py b/uweb3/ext_lib/libs/sqltalk/sqlite/__init__.py deleted file mode 100644 index e28c74e0..00000000 --- a/uweb3/ext_lib/libs/sqltalk/sqlite/__init__.py +++ /dev/null @@ -1,33 +0,0 @@ -#!/usr/bin/python2.5 -"""SQLTalk SQLite interface package.""" - -# Standard modules -import _sqlite3 - -# Application specific modules -import connection - -VERSION_INFO = tuple(map(int, _sqlite3.version.split('.'))) -SQLITE_VERSION_INFO = tuple(map(int, _sqlite3.sqlite_version.split('.'))) - - -def Connect(*args, **kwds): - """Factory function for connection.Connection.""" - kwds['detect_types'] = _sqlite3.PARSE_DECLTYPES - return connection.Connection(*args, **kwds) - - -def ThreadConnect(*args, **kwds): - """Factory function for connection.ThreadedConnection.""" - kwds['detect_types'] = _sqlite3.PARSE_DECLTYPES - return connection.ThreadedConnection(*args, **kwds) - - -DataError = _sqlite3.DataError -DatabaseError = _sqlite3.DatabaseError -Error = _sqlite3.Error -IntegrityError = _sqlite3.IntegrityError -InterfaceError = _sqlite3.InterfaceError -InternalError = _sqlite3.InternalError -NotSupportedError = _sqlite3.NotSupportedError -OperationalError = _sqlite3.OperationalError diff --git a/uweb3/helpers.py b/uweb3/helpers.py deleted file mode 100644 index a744de8b..00000000 --- a/uweb3/helpers.py +++ /dev/null @@ -1,137 +0,0 @@ -import os -import time -import mimetypes -import datetime -from wsgiref.headers import Headers -from uweb3.pagemaker import MimeTypeDict - - -RFC_1123_DATE = '%a, %d %b %Y %T GMT' - -# Search File -def is_accessible(abs_file_path): - return ( - os.path.exists(abs_file_path) and - os.path.isfile(abs_file_path) and - os.access(abs_file_path, os.R_OK) - ) - - -def search_file(relative_file_path, dirs): - for d in dirs: - if not os.path.isabs(d): - d = os.path.abspath(d) + os.sep - - file = os.path.join(d, relative_file_path) - if is_accessible(file): - return file - - -# Header utils -def get_content_length(filename): - stats = os.stat(filename) - return str(stats.st_size) - - -def generate_last_modified(filename): - stats = os.stat(filename) - last_modified = time.strftime("%a, %d %b %Y %H:%M:%sS GMT", time.gmtime(stats.st_mtime)) - return last_modified - - -def get_content_type(mimetype, charset): - if mimetype.startswith('text/') or mimetype == 'application/javascript': - mimetype += '; charset={}'.format(charset) - return mimetype - - -# Response body iterator -def _iter_and_close(file_obj, block_size, charset): - """Yield file contents by block then close the file.""" - while True: - try: - block = file_obj.read(block_size) - if block: - if isinstance(block, bytes): - yield block - else: - yield block.encode(charset) - else: - raise StopIteration - except StopIteration: - file_obj.close() - break - - -def _get_body(filename, method, block_size, charset): - if method == 'HEAD': - return [b''] - return _iter_and_close(open(filename, 'rb'), block_size, charset) - - -# View functions -def static_file_view(env, start_response, filename, block_size, charset, CACHE_DURATION): - method = env['REQUEST_METHOD'].upper() - if method not in ('HEAD', 'GET'): - start_response('405 METHOD NOT ALLOWED', - [('Content-Type', 'text/plain; UTF-8')]) - return [b''] - mimetype, encoding = mimetypes.guess_type(filename) - headers = Headers([]) - - cache_days = CACHE_DURATION.get(mimetype, 0) - expires = datetime.datetime.utcnow() + datetime.timedelta(cache_days) - headers.add_header('Cache-control', f'public, max-age={expires.strftime(RFC_1123_DATE)}') - headers.add_header('Expires', expires.strftime(RFC_1123_DATE)) - if env.get('HTTP_IF_MODIFIED_SINCE'): - if env.get('HTTP_IF_MODIFIED_SINCE') >= generate_last_modified(filename): - start_response('304 ok', headers.items()) - return [b'304'] - headers.add_header('Content-Encodings', encoding) - if mimetype: - headers.add_header('Content-Type', get_content_type(mimetype, charset)) - headers.add_header('Content-Length', get_content_length(filename)) - headers.add_header('Last-Modified', generate_last_modified(filename)) - headers.add_header("Accept-Ranges", "bytes") - start_response('200 OK', headers.items()) - return _get_body(filename, method, block_size, charset) - - -def http404(env, start_response): - start_response('404 Not Found', - [('Content-type', 'text/plain; charset=utf-8')]) - return [b'404 Not Found'] - -#This code is copied and altered from the WSGI static middleware PyPi package -#https://pypi.org/project/wsgi-static-middleware/ -class StaticMiddleware: - CACHE_DURATION = MimeTypeDict({'text': 7, 'image': 30, 'application': 7}) - - def __init__(self, app, static_root, static_dirs=None, - block_size=16*4096, charset='UTF-8'): - self.app = app - self.static_root = static_root.lstrip('/').rstrip('/') - if static_dirs is None: - static_dirs = [os.path.join(os.path.abspath('.'), 'static')] - self.static_dirs = static_dirs - self.charset = charset - self.block_size = block_size - - def __call__(self, env, start_response): - path = env['PATH_INFO'].lstrip('/') - if path.startswith(self.static_root): - relative_file_path = '/'.join(path.split('/')[1:]) - p = os.path.join(self.static_dirs[0], relative_file_path) - if os.path.commonprefix((os.path.realpath(p), self.static_dirs[0])) != self.static_dirs[0]: - return http404(env, start_response) - return self.handle(env, start_response, relative_file_path) - return self.app(env, start_response) - - def handle(self, env, start_response, filename): - abs_file_path = search_file(filename, self.static_dirs) - if abs_file_path: - res = static_file_view(env, start_response, abs_file_path, - self.block_size, self.charset, self.CACHE_DURATION) - return res - else: - return http404(env, start_response) diff --git a/uweb3/ext_lib/__init__.py b/uweb3/libs/__init__.py similarity index 100% rename from uweb3/ext_lib/__init__.py rename to uweb3/libs/__init__.py diff --git a/uweb3/libs/mail.py b/uweb3/libs/mail.py new file mode 100644 index 00000000..818fb729 --- /dev/null +++ b/uweb3/libs/mail.py @@ -0,0 +1,156 @@ +#!/usr/bin/python3 +"""Module to send emails through an smtp server""" + +__author__ = 'Elmer de Looff ' +__version__ = '0.3' + +# Standard modules +import base64 +import os +import smtplib +from email.mime.base import MIMEBase +from email.mime.text import MIMEText +from email.mime.multipart import MIMEMultipart + + +class MailError(Exception): + """Something went wrong sending your email""" + + +class MailSender(object): + """Easy context-interface for sending mail.""" + def __init__(self, host='localhost', port=25, + local_hostname='localhost', timeout=5): + """Sets up the connection to the SMTP server. + + Arguments: + % host: str ~~ 'localhost' + The SMTP hostname to connect to. + % port: int ~~ 25 + Port for the SMTP server. + % local_hostname: str ~~ 'underdark.nl' + The hostname for which we want to send messages. + % timeout: int ~~ 5 + Timeout in seconds. + """ + self.server = None + self.options = {'host': host, 'port': port, + 'local_hostname': local_hostname, 'timeout': timeout} + + def __enter__(self): + """Returns a SendMailContext for sending emails.""" + try: + self.server = smtplib.SMTP(**self.options) + except ConnectionRefusedError as error: + raise SMTPConnectError(error, 'Connection refused.') + return SendMailContext(self.server) + + def __exit__(self, *_exc_args): + """Done sending mail, closes the smtp server connection.""" + self.server.quit() + + +class SendMailContext(object): + """Context to use for sending emails.""" + def __init__(self, server): + """Stores the server object locally.""" + self.server = server + + def Text(self, recipients, subject, content, + sender=None, reply_to=None, charset='utf8'): + """Send a text message + + Arguments: + @ recipients: str / list of str + Email address(es) of all TO: recipients. + @ subject: str + Email subject + @ content: str + Body of the email + % sender: str ~~ self.Noreply() + The sender email addres, this defaults to the no-reply address. + % reply_to: str ~~ None + Optional reply-to address that differs from sender. + % charset: str ~~ 'utf8' + Character set to encode mail to. + """ + message = MIMEMultipart() + message['From'] = sender or self.Noreply() + message['To'] = self.ParseRecipients(recipients) + message['Subject'] = ' '.join(subject.strip().split()) + message.attach(MIMEText(content.encode(charset), 'plain', charset)) + if reply_to: + message['Reply-to'] = self.ParseRecipients(reply_to) + self.server.sendmail(message['From'], recipients, message.as_string()) + + def Attachments(self, recipients, subject, content, + attachments, sender=None, reply_to=None, charset='utf8'): + """Sends email with attachments. + + Arguments like `Text()` but adds `attachments` after content. This should + be a list of `str` (filename), `file` or 2-tuples with name and content. + Content in case of 2-tuple can be `str` or any file-like object. + """ + message = MIMEMultipart() + message['From'] = sender or self.Noreply() + message['To'] = self.ParseRecipients(recipients) + message['Subject'] = ' '.join(subject.strip().split()) + if reply_to: + message['Reply-to'] = self.ParseRecipients(reply_to) + message.attach(MIMEText(content.encode(charset), 'plain', charset)) + if isinstance(attachments, str): + message.attach(self.ParseAttachment(attachments)) + else: + for attachment in attachments: + message.attach(self.ParseAttachment(attachment)) + self.server.sendmail(message['From'], recipients, str(message)) + + @staticmethod + def ParseAttachment(attachment): + """Parses an attachment descriptor and returns a MIMEBase part for email.""" + if isinstance(attachment, tuple): + name, contents = attachment + if hasattr(contents, 'read'): + contents = contents.read() + elif isinstance(attachment, str): + name = os.path.basename(attachment) + contents = file(attachment, 'rb').read() + elif isinstance(attachment, file): + name = os.path.basename(attachment.name) + attachment.seek(0) + contents = attachment.read() + + part = MIMEBase('application', 'octet-stream') + part.set_payload(Wrap(base64.b64encode(contents))) + part.add_header('Content-Transfer-Encoding', 'base64') + part.add_header('Content-Disposition', 'attachment; filename="%s"' % name) + return part + + @staticmethod + def ParseRecipients(recipients): + """Ensures multiple recipients are returned as a string without newlines.""" + if isinstance(recipients, str): + return StripNewlines(recipients) + return ', '.join(map(StripNewlines, recipients)) + + def Noreply(self): + """Returns the no-reply email address for the configured local hostname.""" + return 'no-reply ' % self.server.local_hostname + + +def StripNewlines(text): + """Replaces newlines and tabs with a single space.""" + return ' '.join(text.strip().split()) + + +def Wrap(content, cols=76): + """Wraps multipart mime content into 76 column lines for niceness.""" + lines = [] + while content: + lines.append(content[:cols]) + content = content[cols:] + return '\r\n'.join(lines) + + +SMTPConnectError = smtplib.SMTPConnectError +SMTPRecipientsRefused = smtplib.SMTPRecipientsRefused diff --git a/uweb3/ext_lib/libs/safestring/__init__.py b/uweb3/libs/safestring/__init__.py similarity index 79% rename from uweb3/ext_lib/libs/safestring/__init__.py rename to uweb3/libs/safestring/__init__.py index db997e90..70b5b403 100644 --- a/uweb3/ext_lib/libs/safestring/__init__.py +++ b/uweb3/libs/safestring/__init__.py @@ -20,24 +20,30 @@ type. Handy escape() functions are present to do manual escaping if required. """ -#TODO: logger geen enters -#bash injection -#mysql escaping +#TODO: logger, dont output Enters +# bash injection +# mysql escaping __author__ = 'Jan Klopper (jan@underdark.nl)' __version__ = 0.1 import html +from json import JSONEncoder import json import urllib.parse as urlparse import re -from ast import literal_eval -from sqlalchemy import text + +# json encoder modules +import datetime +import uuid +from uweb3 import model + class Basesafestring(str): """Base safe string class This does not signal any safety against injection itself, use the child - classes instead!""" - "" + classes instead! + """ + def __add__(self, other): """Adds a second string to this string, upgrading it in the process""" data = ''.join(( # do not use the __add__ since that creates a loop @@ -50,11 +56,10 @@ def __upgrade__(self, other): the current object""" if type(other) == self.__class__: #same type, easy, lets add return other - elif isinstance(other, Basesafestring): # lets unescape the other 'safe' type, + if isinstance(other, Basesafestring): # lets unescape the other 'safe' type, otherdata = other.unescape(other) # its escaping is not needed for his context return self.escape(otherdata) # escape it using our context - else: - return self.escape(str(other)) # escape it using our context + return self.escape(str(other)) # escape it using our context def __new__(cls, data, **kwargs): return super().__new__(cls, @@ -112,21 +117,15 @@ class SQLSAFE(Basesafestring): PLACEHOLDERS_REGEX = re.compile(r"""\?+""") QUOTES_REGEX = re.compile(r"""([\"'])(?:(?=(\\?))\2.)*?\1""", re.DOTALL) - def __new__(cls, data, values=(), *args, **kwargs): - return super().__new__(cls, - cls.escape(cls, str(data), values) if 'unsafe' in kwargs else data) - def __upgrade__(self, other): - """Upgrade a given object to be as safe, and in the same safety context as - the current object""" - if type(other) == self.__class__: #same type, easy, lets add - return other - elif isinstance(other, Basesafestring): # lets unescape the other 'safe' type, - otherdata = other.unescape(other) # its escaping is not needed for his context - return self.sanitize(otherdata) # escape it using our context - else: - other = " " + other - return self.sanitize(other, with_quotes=False) + """Upgrade a given object to be as safe, and in the same safety context as + the current object + """ + if type(other) == self.__class__: #same type, easy, lets add + return other + elif isinstance(other, Basesafestring): # lets unescape the other 'safe' type, + return self.sanitize(other.unescape(other)) # escape it using our context + return self.sanitize(" " + other, with_quotes=False) @classmethod def sanitize(cls, value, with_quotes=True): @@ -178,10 +177,19 @@ def unescape(self, value): # what follows are the actual useable classes that are safe in specific contexts +class Unsafestring(Basesafestring): + """This class removes any escaping done""" + + def escape(self, data): + return data + + def unescape(self, data): + return data + + class HTMLsafestring(Basesafestring): """This class signals that the content is HTML safe""" - def escape(self, data): return html.escape(data) @@ -194,18 +202,44 @@ class JSONsafestring(Basesafestring): Most of this will be handled by just feeding regular python objects into json.dumps, but for some outputs this might be handy. Eg, when outputting - partial json into dynamic generated javascript files""" + partial json into dynamic generated javascript files + """ + + def __new__(cls, data, **kwargs): + if isinstance(data, str): + data = cls.escape(cls, str(data)) if 'unsafe' in kwargs else data + else: + data = json.dumps(data, cls=JsonEncoder) + return super().__new__(cls, data) def escape(self, data): - if not isinstance(data, str): - raise TypeError - return json.dumps(data) + return json.dumps(data, cls=JsonEncoder) def unescape(self, data): if not isinstance(data, str): raise TypeError - data = json.loads(data) - return data + return json.loads(data) + +class JsonEncoder(json.JSONEncoder): + def default (self, o): + if isinstance(o, datetime.datetime): + return o.strftime('%F %T') + if isinstance(o, datetime.date): + return o.strftime('%F') + if isinstance(o, datetime.time): + return o.strftime('%T') + if isinstance(o, uuid.UUID): + return str(o) + if hasattr(o, "__json__"): + return str(o.__json__()) + if hasattr(o, "__html__"): + return str(o.__html__()) + if hasattr(o, "__dict__"): + return o.__dict__ + try: + return super().default(o) + except TypeError: + return str(o) class URLqueryargumentsafestring(Basesafestring): @@ -222,7 +256,8 @@ def unescape(self, data): class URLsafestring(Basesafestring): """This class signals that the content is URL safe, for use in http headers - like redirects, but also calls to wget or the like""" + like redirects, but also calls to wget or the like + """ def escape(self, data): """Drops everything that does not fit in a url @@ -246,8 +281,9 @@ def unescape(self, data): class EmailAddresssafestring(Basesafestring): """This class signals that the content is safe Email address - ITs usefull when sending out emails or constructing email headers - Email Header injection is subverted.""" + Its usefull when sending out emails or constructing email headers + Email Header injection is subverted. + """ def escape(self, data): """Drops everything that does not fit in an email address""" diff --git a/uweb3/ext_lib/libs/safestring/test.py b/uweb3/libs/safestring/test.py similarity index 100% rename from uweb3/ext_lib/libs/safestring/test.py rename to uweb3/libs/safestring/test.py diff --git a/uweb3/ext_lib/libs/sqltalk/__init__.py b/uweb3/libs/sqltalk/__init__.py similarity index 80% rename from uweb3/ext_lib/libs/sqltalk/__init__.py rename to uweb3/libs/sqltalk/__init__.py index 2981d79f..cf45f629 100644 --- a/uweb3/ext_lib/libs/sqltalk/__init__.py +++ b/uweb3/libs/sqltalk/__init__.py @@ -1,11 +1,11 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python3 """Easy use SQL abstraction module. Returns a custom QueryResult object that holds the result, query, used character set and various other small statistics. The QueryResult object also support pivoting and subselects -Currently only implements a MySQL abstraction module with a stripped down +Currently implements a MySQL and sqlite abstraction module with a stripped down version of MySQLdb internally. example usage: diff --git a/uweb3/libs/sqltalk/mysql/__init__.py b/uweb3/libs/sqltalk/mysql/__init__.py new file mode 100644 index 00000000..d471c984 --- /dev/null +++ b/uweb3/libs/sqltalk/mysql/__init__.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +"""SQLTalk MySQL interface package. + +Functions: + Connect: Connects to a MySQL server and returns a connection object. + Refer to the documentation enclosed in the connections module for + argument information. +""" +__author__ = 'Jan Klopper ' +__version__ = '0.10' + +# Application specific modules +from . import connection + +def Connect(*args, **kwargs): + """Factory function for connection.Connection.""" + return connection.Connection(*args, **kwargs) diff --git a/uweb3/ext_lib/libs/sqltalk/mysql/connection.py b/uweb3/libs/sqltalk/mysql/connection.py similarity index 89% rename from uweb3/ext_lib/libs/sqltalk/mysql/connection.py rename to uweb3/libs/sqltalk/mysql/connection.py index e9f059b7..074156b4 100644 --- a/uweb3/ext_lib/libs/sqltalk/mysql/connection.py +++ b/uweb3/libs/sqltalk/mysql/connection.py @@ -3,8 +3,9 @@ a MySQL database. From this connection, cursor objects can be created, which use the escaping and character encoding facilities offered by the connection. """ -__author__ = 'Elmer de Looff ' -__version__ = '0.16' +__author__ = ('Elmer de Looff ', + 'Jan Klopper ') +__version__ = '0.17' # Standard modules import pymysql @@ -13,8 +14,8 @@ import weakref # Application specific modules -from . import constants -from . import converters +from pymysql import constants +from pymysql import converters from . import cursor from .. import sqlresult @@ -118,7 +119,7 @@ def StringDecoder(string): encoders = {} converts = {} - conversions = converters.CONVERSIONS + conversions = converters.conversions self.string_decoder = _GetStringDecoder() # if use_unicode: @@ -151,7 +152,6 @@ def StringDecoder(string): # self.encoders[unicode] = self.unicode_literal = _GetUnicodeLiteral() self.converter = conversions - self.server_version = tuple(map(int, self.get_server_info().split('.')[:2])) if sql_mode: self.SetSqlMode(sql_mode) @@ -159,11 +159,11 @@ def StringDecoder(string): self.transactional = bool(self.server_capabilities & constants.CLIENT.TRANSACTIONS) - self._autocommit = None + self.autocommit_mode = None if autocommit is not None: - self.autocommit = autocommit + self.autocommit_mode = autocommit else: - self.autocommit = not self.transactional + self.autocommit_mode = not self.transactional def __enter__(self): """Refreshes the connection and returns a cursor, starting a transaction.""" @@ -181,15 +181,17 @@ def __exit__(self, exc_type, exc_value, _exc_traceback): self.ResetTransactionTimer() if exc_type: self.rollback() - self.logger.exception( - 'The transaction was rolled back after an exception.\n' - 'Server: %s\nQueries in transaction (last one triggered):\n\n%s', - self.get_host_info(), - '\n\n'.join(self.queries)) + if self.debug: + self.logger.exception( + 'The transaction was rolled back after an exception.\n' + 'Server: %s\nQueries in transaction (last one triggered):\n\n%s', + self.get_host_info(), + '\n\n'.join(self.queries)) else: self.commit() - self.logger.debug( - 'Transaction committed (server: %r).', self.get_host_info()) + if self.debug: + self.logger.debug( + 'Transaction committed (server: %r).', self.get_host_info()) self.lock.release() def CurrentDatabase(self): @@ -200,11 +202,10 @@ def EscapeField(self, field): """Returns a SQL escaped field or table name.""" if not field: return '' - elif isinstance(field, str): + if isinstance(field, str): fields = '.'.join('`%s`' % f.replace('`', '``') for f in field.split('.')) return fields.replace('`*`', '*') - else: - return map(self.EscapeField, field) + return map(self.EscapeField, field) def EscapeValues(self, obj): """Escapes any object passed in following the encoders dictionary. @@ -212,6 +213,8 @@ def EscapeValues(self, obj): Sequences and mappings will only have their contents escaped. All strings will be encoded to the connection's character set. """ + if isinstance(obj, tuple) or isinstance(obj, list): + return list(map(lambda x: self.escape(x, self.encoders), obj)) return self.escape(obj, self.encoders) def Info(self): @@ -225,15 +228,16 @@ def Info(self): 'charset': self.charset, 'server': self.ServerInfo()} - def Query(self, query_string): + def Query(self, query_string, cur=None): self.counter_queries += 1 if isinstance(query_string, str): query_string = query_string.encode(self.charset) - cur = cursor.Cursor(self) + if not cur: + cur = cursor.Cursor(self) cur.execute(query_string) stored_result = cur.fetchall() if stored_result: - fields = stored_result[0].keys() + fields = list(stored_result[0]) else: fields = [] return sqlresult.ResultSet( @@ -250,16 +254,11 @@ def ServerInfo(self): def SetSqlMode(self, sql_mode): """Set the connection sql_mode. See MySQL documentation for legal values.""" - if self.server_version < (4, 1): - raise self.NotSupportedError('server is too old to set sql_mode') self.Query('SET SESSION sql_mode=%s' % self.EscapeValues(sql_mode)) def ShowWarnings(self): """Return detailed information about warnings as a sequence of tuples of - (Level, Code, Message). This is only supported in MySQL-4.1 and up. - If your server is an earlier version, an empty sequence is returned.""" - if self.server_version < (4, 1): - return () + (Level, Code, Message).""" return self.Query('SHOW WARNINGS') def StartTransactionTimer(self, delay=60): @@ -283,7 +282,7 @@ def ResetTransactionTimer(self): def _GetAutocommitState(self): """This returns the current setting for autocommiting transactions.""" - return self._autocommit + return self.autocommit_mode def _GetCharacterSet(self): """This configures the character set used by this connection. @@ -296,9 +295,12 @@ def _SetAutocommitState(self, state): """This sets the autocommit mode on the connection. This is False by default if the database supports transactions.""" - self.ping(state) + try: + self.ping(reconnect=True) + except: + self.connect(sock=None) super(Connection, self).autocommit(state) - self._autocommit = state + self.autocommit_mode = state def _SetCharacterSet(self, charset): """This sets the character set, refer to _GetCharacterSet for doc.""" @@ -306,13 +308,14 @@ def _SetCharacterSet(self, charset): super(Connection, self).set_charset(charset) self._charset = charset + # Error classes taken from PyMySQL Error = pymysql.Error InterfaceError = pymysql.InterfaceError DatabaseError = pymysql.DatabaseError DataError = pymysql.DataError OperationalError = pymysql.OperationalError - IntegrityError = pymysql.IntegrityError + IntegrityError = pymysql.err.IntegrityError InternalError = pymysql.InternalError ProgrammingError = pymysql.ProgrammingError NotSupportedError = pymysql.NotSupportedError diff --git a/uweb3/ext_lib/libs/sqltalk/mysql/cursor.py b/uweb3/libs/sqltalk/mysql/cursor.py similarity index 97% rename from uweb3/ext_lib/libs/sqltalk/mysql/cursor.py rename to uweb3/libs/sqltalk/mysql/cursor.py index 8cc119a0..7b826430 100644 --- a/uweb3/ext_lib/libs/sqltalk/mysql/cursor.py +++ b/uweb3/libs/sqltalk/mysql/cursor.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python """SQLTalk MySQL Cursor class.""" __author__ = 'Elmer de Looff ' __version__ = '0.13' @@ -13,7 +13,7 @@ class ReturnObject(tuple): def __new__(cls, connection, results): """Creates the immutable tuple.""" - return super(ReturnObject, cls).__new__(cls, tuple(results)) + return super().__new__(cls, tuple(results)) def __init__(self, connection, results): """Adds the required attributes.""" @@ -54,10 +54,7 @@ def _Execute(self, query): # of other escaping things to start working properly. # Refer to MySQLdb.cursor code (~line 151) to see how this works. self._LogQuery(query) - self.execute(query.strip()) - result = ReturnObject(self.connection, self.fetchall()) - self._ProcessWarnings(result) - return result + return self.connection.Query(query.strip(), self) def _LogQuery(self, query): connection = self.connection @@ -206,7 +203,8 @@ def Insert(self, table, values, escape=True): return self._Execute(query) def Select(self, table, fields=None, conditions=None, order=None, - group=None, limit=None, offset=0, escape=True, totalcount=False): + group=None, limit=None, offset=0, escape=True, totalcount=False, + distinct=False): """Select fields from table that match the conditions, ordered and limited. Arguments: @@ -232,20 +230,21 @@ def Select(self, table, fields=None, conditions=None, order=None, totalcount: boolean. If this is set to True, queries with a LIMIT applied will have the full number of matching rows on the affected_rows attribute of the resultset. + distinct: bool (optional). Performs a DISTINCT query if set to True. Returns: sqlresult.ResultSet object. """ field_escape = self.connection.EscapeField if escape else lambda x: x - result = self._Execute('SELECT %s %s FROM %s WHERE %s %s %s %s' % ( + result = self._Execute('SELECT %s %s %s FROM %s WHERE %s %s %s %s' % ( 'SQL_CALC_FOUND_ROWS' if totalcount and limit is not None else '', + 'DISTINCT' if distinct else '', self._StringFields(fields, field_escape), self._StringTable(table, field_escape), self._StringConditions(conditions, field_escape), self._StringGroup(group, field_escape), self._StringOrder(order, field_escape), self._StringLimit(limit, offset))) - if totalcount and limit is not None: result.affected = self._Execute('SELECT FOUND_ROWS()')[0][0] return result diff --git a/uweb3/libs/sqltalk/sqlite/__init__.py b/uweb3/libs/sqltalk/sqlite/__init__.py new file mode 100644 index 00000000..dc904251 --- /dev/null +++ b/uweb3/libs/sqltalk/sqlite/__init__.py @@ -0,0 +1,33 @@ +#!/usr/bin/python3 +"""SQLTalk SQLite interface package.""" + +# Standard modules +import sqlite3 + +# Application specific modules +from . import connection + +VERSION_INFO = tuple(map(int, sqlite3.version.split('.'))) +SQLITE_VERSION_INFO = tuple(map(int, sqlite3.sqlite_version.split('.'))) + + +def Connect(*args, **kwds): + """Factory function for connection.Connection.""" + kwds['detect_types'] = sqlite3.PARSE_DECLTYPES + return connection.Connection(*args, **kwds) + + +def ThreadConnect(*args, **kwds): + """Factory function for connection.ThreadedConnection.""" + kwds['detect_types'] = sqlite3.PARSE_DECLTYPES + return connection.ThreadedConnection(*args, **kwds) + + +DataError = sqlite3.DataError +DatabaseError = sqlite3.DatabaseError +Error = sqlite3.Error +IntegrityError = sqlite3.IntegrityError +InterfaceError = sqlite3.InterfaceError +InternalError = sqlite3.InternalError +NotSupportedError = sqlite3.NotSupportedError +OperationalError = sqlite3.OperationalError diff --git a/uweb3/ext_lib/libs/sqltalk/sqlite/connection.py b/uweb3/libs/sqltalk/sqlite/connection.py similarity index 84% rename from uweb3/ext_lib/libs/sqltalk/sqlite/connection.py rename to uweb3/libs/sqltalk/sqlite/connection.py index 740b4250..e9ff1191 100644 --- a/uweb3/ext_lib/libs/sqltalk/sqlite/connection.py +++ b/uweb3/libs/sqltalk/sqlite/connection.py @@ -1,29 +1,27 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python3 """This module implements the Connection class, which sets up a connection to an SQLite database. From this connection, cursor objects can be created, which use the escaping and character encoding facilities offered by the connection. """ -from __future__ import with_statement - __author__ = 'Elmer de Looff ' __version__ = '0.3' # Standard modules -import _sqlite3 +import sqlite3 import logging import os -import Queue +import queue import threading # Application specific modules -import converters -import cursor +from . import converters +from . import cursor COMMIT = '----COMMIT' ROLLBACK = '----ROLLBACK' NAMED_TYPE_SELECT = 'SELECT `name` FROM `sqlite_master` where `type`=?' -class Connection(_sqlite3.Connection): +class Connection(sqlite3.Connection): def __init__(self, *args, **kwds): db_name = os.path.splitext(os.path.split(args[0])[1])[0] self.logger = logging.getLogger('sqlite_%s' % db_name) @@ -33,7 +31,7 @@ def __init__(self, *args, **kwds): self.logger.setLevel(logging.WARNING) if kwds.pop('disable_log', False): self.logger.disable_logger = True - _sqlite3.Connection.__init__(self, *args, **kwds) + sqlite3.Connection.__init__(self, *args, **kwds) def __enter__(self): """Starts a transaction.""" @@ -50,22 +48,22 @@ def __exit__(self, exc_type, _exc_value, _exc_traceback): self.logger.debug('Transaction committed.') def commit(self): - _sqlite3.Connection.commit(self) - - def execute(self, query, args=()): - return _sqlite3.Connection.execute(self, query, args) - - def executemany(self, query, args=()): - return _sqlite3.Connection.executemany(self, query, args) + sqlite3.Connection.commit(self) def rollback(self): - _sqlite3.Connection.rollback(self) + sqlite3.Connection.rollback(self) @staticmethod def EscapeField(field): """Returns a SQL escaped field or table name.""" return '.'.join('`%s`' % f.replace('`', '``') for f in field.split('.')) + def EscapeValues(self, obj): + """We do not escape here, we simple return the value and allow the query + engine to escape using parameters. + """ + return obj + def ShowTables(self): result = self.execute(NAMED_TYPE_SELECT, ('table',)).fetchall() return [row[0] for row in result] @@ -86,7 +84,7 @@ def __init__(self, *args, **kwds): self.sqlite_args = args self.sqlite_kwds = kwds - self.queries = Queue.Queue(1) + self.queries = queue.queue(1) self.transaction_lock = threading.RLock() self.daemon = True self.start() @@ -111,13 +109,13 @@ def commit(self): def execute(self, query, args=()): with self.transaction_lock: - response = Queue.Queue() + response = queue.queue() self.queries.put((query, args, response, False)) return self._ProcessResponse(response) def executemany(self, query, args=()): with self.transaction_lock: - response = Queue.Queue() + response = queue.queue() self.queries.put((query, args, response, True)) return self._ProcessResponse(response) @@ -178,11 +176,11 @@ def fetchall(self): #FIXME(Elmer): This needs defining in one place, not in each and every file. -DataError = _sqlite3.DataError -DatabaseError = _sqlite3.DatabaseError -Error = _sqlite3.Error -IntegrityError = _sqlite3.IntegrityError -InterfaceError = _sqlite3.InterfaceError -InternalError = _sqlite3.InternalError -NotSupportedError = _sqlite3.NotSupportedError -OperationalError = _sqlite3.OperationalError +DataError = sqlite3.DataError +DatabaseError = sqlite3.DatabaseError +Error = sqlite3.Error +IntegrityError = sqlite3.IntegrityError +InterfaceError = sqlite3.InterfaceError +InternalError = sqlite3.InternalError +NotSupportedError = sqlite3.NotSupportedError +OperationalError = sqlite3.OperationalError diff --git a/uweb3/ext_lib/libs/sqltalk/sqlite/converters.py b/uweb3/libs/sqltalk/sqlite/converters.py similarity index 91% rename from uweb3/ext_lib/libs/sqltalk/sqlite/converters.py rename to uweb3/libs/sqltalk/sqlite/converters.py index f2f64e4b..8bf0187d 100644 --- a/uweb3/ext_lib/libs/sqltalk/sqlite/converters.py +++ b/uweb3/libs/sqltalk/sqlite/converters.py @@ -1,4 +1,4 @@ -import _sqlite3 +import sqlite3 import datetime import pytz import time @@ -100,8 +100,8 @@ def ConvertTimestamp(date_obj): return INTERPRET_AS_UTC(datetime.datetime(*time_tuple)) -_sqlite3.register_adapter(datetime.date, AdaptDate) -_sqlite3.register_adapter(datetime.datetime, AdaptDatetime) -_sqlite3.register_adapter(time.struct_time, AdaptTimeStruct) -_sqlite3.register_converter('DATE', ConvertDate) -_sqlite3.register_converter('TIMESTAMP', ConvertTimestamp) +sqlite3.register_adapter(datetime.date, AdaptDate) +sqlite3.register_adapter(datetime.datetime, AdaptDatetime) +sqlite3.register_adapter(time.struct_time, AdaptTimeStruct) +sqlite3.register_converter('DATE', ConvertDate) +sqlite3.register_converter('TIMESTAMP', ConvertTimestamp) diff --git a/uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py b/uweb3/libs/sqltalk/sqlite/cursor.py similarity index 93% rename from uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py rename to uweb3/libs/sqltalk/sqlite/cursor.py index f6410a7d..440ea0ba 100644 --- a/uweb3/ext_lib/libs/sqltalk/sqlite/cursor.py +++ b/uweb3/libs/sqltalk/sqlite/cursor.py @@ -1,23 +1,24 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python3 """SQLTalk SQLite Cursor class.""" __author__ = 'Elmer de Looff ' __version__ = '0.4' # Custom modules -from ext_libs.libs.sqltalk import sqlresult +from .. import sqlresult class Cursor(object): def __init__(self, connection): self.connection = connection + self.cursor = connection.cursor() def Execute(self, query, args=(), many=False): try: if many: - result = self.connection.executemany(query, args) + result = self.cursor.executemany(query, args) else: - result = self.connection.execute(query, args) + result = self.cursor.execute(query, args) except Exception: self.connection.logger.exception('Exception during query execution') raise @@ -70,14 +71,14 @@ def Select(self, table, fields=None, conditions=None, order=None, group=None, Returns: sqlresult.ResultSet object. """ - if isinstance(table, basestring): + if isinstance(table, str): table = self.connection.EscapeField(table) else: table = ', '.join(map(self.connection.EscapeField, table)) if fields is None: fields = '*' - elif isinstance(fields, basestring): + elif isinstance(fields, str): fields = self.connection.EscapeField(fields) else: fields = ', '.join(map(self.connection.EscapeField, fields)) @@ -91,7 +92,7 @@ def Select(self, table, fields=None, conditions=None, order=None, group=None, if order is not None: orders = [] for rule in order: - if isinstance(rule, basestring): + if isinstance(rule, str): orders.append(self.connection.EscapeField(rule)) else: orders.append('%s %s' % diff --git a/uweb3/ext_lib/libs/sqltalk/sqlresult.py b/uweb3/libs/sqltalk/sqlresult.py similarity index 92% rename from uweb3/ext_lib/libs/sqltalk/sqlresult.py rename to uweb3/libs/sqltalk/sqlresult.py index 5e61d9b4..35e4fc31 100644 --- a/uweb3/ext_lib/libs/sqltalk/sqlresult.py +++ b/uweb3/libs/sqltalk/sqlresult.py @@ -10,11 +10,11 @@ FieldError: Field- index or name does not exist. NotSupportedError: Operation is not supported """ -__author__ = 'Elmer de Looff ' -__version__ = '1.3' +__author__ = ('Elmer de Looff ', + 'Jan Klopper ') +__version__ = '1.4' # Standard modules -import itertools import operator GET_FIELD_NAME = operator.itemgetter(0) @@ -116,7 +116,7 @@ def itervalues(self): return iter(self._values) def iteritems(self): - return itertools.izip(self._fields, self._values) + return zip(self._fields, self._values) def keys(self): return self._fields[:] @@ -204,9 +204,8 @@ def __init__(self, query='', charset='', result=None, fields=None, Number of affected rows from this operation. % charset: str ~~ '' Character set used by the connection that executed this operation. - % fields: tuple of tuples of strings ~~ None - Description of fields involved in this operation. Tuples of strings as - per the Python DB API (v2). + % fields: tuple of strings ~~ None + Description of fields involved in this operation. % insertid: int ~~ 0 Auto-increment ID that was generated upon the last insert. % query ~~ '' @@ -222,11 +221,10 @@ def __init__(self, query='', charset='', result=None, fields=None, if result: self.fields = fields - self._fieldnames = fieldnames = map(GET_FIELD_NAME, fields) - self.result = [row_class(fieldnames, row) for row in result] + self.raw = result + self.result = [row_class(fields, row.values()) for row in result] else: self.fields = () - self._fieldnames = [] self.result = [] def __eq__(self, other): @@ -266,7 +264,7 @@ def __getitem__(self, item): except TypeError: # The item type is incorrect, try grabbing a column for this fieldname. try: - index = self._fieldnames.index(item) + index = self._fields.index(item) return tuple(row[index] for row in self.result) except ValueError: raise FieldError('Bad field name: %r.' % item) @@ -285,7 +283,7 @@ def __nonzero__(self): def __repr__(self): """Returns a string representation of the ResultSet.""" - return '%s instance: %d rows%s' % ( + return '%s instance: %d row%s' % ( self.__class__.__name__, len(self.result), 's'[len(self.result) == 1:]) def FilterRowsByFields(self, *fields): @@ -302,7 +300,7 @@ def FilterRowsByFields(self, *fields): ResultRow: Each ResultRow contains only the filtered fields. """ try: - indices = tuple(self._fieldnames.index(field) for field in fields) + indices = tuple(self._fields.index(field) for field in fields) except ValueError: raise FieldError('Bad fieldnames in filter request.') for row in self: @@ -310,7 +308,7 @@ def FilterRowsByFields(self, *fields): def PopField(self, field): try: - self._fieldnames.remove(field) + self._fields.remove(field) except ValueError: raise FieldError('Fieldname %r does not occur in the ResultSet.' % field) return [row.pop(field) for row in self] @@ -321,4 +319,4 @@ def PopRow(self, row_index): @property def fieldnames(self): """Returns a tuple of the fieldnames that are in this ResultSet.""" - return tuple(self._fieldnames) + return tuple(self._fields) diff --git a/uweb3/ext_lib/libs/sqltalk/sqlresult_test.py b/uweb3/libs/sqltalk/sqlresult_test.py similarity index 100% rename from uweb3/ext_lib/libs/sqltalk/sqlresult_test.py rename to uweb3/libs/sqltalk/sqlresult_test.py diff --git a/uweb3/ext_lib/libs/urlsplitter/__init__.py b/uweb3/libs/urlsplitter/__init__.py similarity index 100% rename from uweb3/ext_lib/libs/urlsplitter/__init__.py rename to uweb3/libs/urlsplitter/__init__.py diff --git a/uweb3/ext_lib/libs/urlsplitter/test.py b/uweb3/libs/urlsplitter/test.py similarity index 100% rename from uweb3/ext_lib/libs/urlsplitter/test.py rename to uweb3/libs/urlsplitter/test.py diff --git a/uweb3/libs/utils.py b/uweb3/libs/utils.py new file mode 100644 index 00000000..477164e3 --- /dev/null +++ b/uweb3/libs/utils.py @@ -0,0 +1,774 @@ +# -*- coding: utf-8 -*- +""" + werkzeug.utils + ~~~~~~~~~~~~~~ + + This module implements various utilities for WSGI applications. Most of + them are used by the request and response wrappers but especially for + middleware development it makes sense to use them without the wrappers. + + :copyright: 2007 Pallets + :license: BSD-3-Clause +""" +import codecs +import os +import pkgutil +import re +import sys + +from ._compat import iteritems +from ._compat import PY2 +from ._compat import reraise +from ._compat import string_types +from ._compat import text_type +from ._compat import unichr +from ._internal import _DictAccessorProperty +from ._internal import _missing +from ._internal import _parse_signature + +try: + from html.entities import name2codepoint +except ImportError: + from htmlentitydefs import name2codepoint + + +_format_re = re.compile(r"\$(?:(%s)|\{(%s)\})" % (("[a-zA-Z_][a-zA-Z0-9_]*",) * 2)) +_entity_re = re.compile(r"&([^;]+);") +_filename_ascii_strip_re = re.compile(r"[^A-Za-z0-9_.-]") +_windows_device_files = ( + "CON", + "AUX", + "COM1", + "COM2", + "COM3", + "COM4", + "LPT1", + "LPT2", + "LPT3", + "PRN", + "NUL", +) + + +class cached_property(property): + """A decorator that converts a function into a lazy property. The + function wrapped is called the first time to retrieve the result + and then that calculated result is used the next time you access + the value:: + + class Foo(object): + + @cached_property + def foo(self): + # calculate something important here + return 42 + + The class has to have a `__dict__` in order for this property to + work. + """ + + # implementation detail: A subclass of python's builtin property + # decorator, we override __get__ to check for a cached value. If one + # chooses to invoke __get__ by hand the property will still work as + # expected because the lookup logic is replicated in __get__ for + # manual invocation. + + def __init__(self, func, name=None, doc=None): + self.__name__ = name or func.__name__ + self.__module__ = func.__module__ + self.__doc__ = doc or func.__doc__ + self.func = func + + def __set__(self, obj, value): + obj.__dict__[self.__name__] = value + + def __get__(self, obj, type=None): + if obj is None: + return self + value = obj.__dict__.get(self.__name__, _missing) + if value is _missing: + value = self.func(obj) + obj.__dict__[self.__name__] = value + return value + + +class environ_property(_DictAccessorProperty): + """Maps request attributes to environment variables. This works not only + for the Werzeug request object, but also any other class with an + environ attribute: + + >>> class Test(object): + ... environ = {'key': 'value'} + ... test = environ_property('key') + >>> var = Test() + >>> var.test + 'value' + + If you pass it a second value it's used as default if the key does not + exist, the third one can be a converter that takes a value and converts + it. If it raises :exc:`ValueError` or :exc:`TypeError` the default value + is used. If no default value is provided `None` is used. + + Per default the property is read only. You have to explicitly enable it + by passing ``read_only=False`` to the constructor. + """ + + read_only = True + + def lookup(self, obj): + return obj.environ + + +class header_property(_DictAccessorProperty): + """Like `environ_property` but for headers.""" + + def lookup(self, obj): + return obj.headers + + +class HTMLBuilder(object): + """Helper object for HTML generation. + + Per default there are two instances of that class. The `html` one, and + the `xhtml` one for those two dialects. The class uses keyword parameters + and positional parameters to generate small snippets of HTML. + + Keyword parameters are converted to XML/SGML attributes, positional + arguments are used as children. Because Python accepts positional + arguments before keyword arguments it's a good idea to use a list with the + star-syntax for some children: + + >>> html.p(class_='foo', *[html.a('foo', href='foo.html'), ' ', + ... html.a('bar', href='bar.html')]) + u'

foo bar

' + + This class works around some browser limitations and can not be used for + arbitrary SGML/XML generation. For that purpose lxml and similar + libraries exist. + + Calling the builder escapes the string passed: + + >>> html.p(html("")) + u'

<foo>

' + """ + + _entity_re = re.compile(r"&([^;]+);") + _entities = name2codepoint.copy() + _entities["apos"] = 39 + _empty_elements = { + "area", + "base", + "basefont", + "br", + "col", + "command", + "embed", + "frame", + "hr", + "img", + "input", + "keygen", + "isindex", + "link", + "meta", + "param", + "source", + "wbr", + } + _boolean_attributes = { + "selected", + "checked", + "compact", + "declare", + "defer", + "disabled", + "ismap", + "multiple", + "nohref", + "noresize", + "noshade", + "nowrap", + } + _plaintext_elements = {"textarea"} + _c_like_cdata = {"script", "style"} + + def __init__(self, dialect): + self._dialect = dialect + + def __call__(self, s): + return escape(s) + + def __getattr__(self, tag): + if tag[:2] == "__": + raise AttributeError(tag) + + def proxy(*children, **arguments): + buffer = "<" + tag + for key, value in iteritems(arguments): + if value is None: + continue + if key[-1] == "_": + key = key[:-1] + if key in self._boolean_attributes: + if not value: + continue + if self._dialect == "xhtml": + value = '="' + key + '"' + else: + value = "" + else: + value = '="' + escape(value) + '"' + buffer += " " + key + value + if not children and tag in self._empty_elements: + if self._dialect == "xhtml": + buffer += " />" + else: + buffer += ">" + return buffer + buffer += ">" + + children_as_string = "".join( + [text_type(x) for x in children if x is not None] + ) + + if children_as_string: + if tag in self._plaintext_elements: + children_as_string = escape(children_as_string) + elif tag in self._c_like_cdata and self._dialect == "xhtml": + children_as_string = ( + "/**/" + ) + buffer += children_as_string + "" + return buffer + + return proxy + + def __repr__(self): + return "<%s for %r>" % (self.__class__.__name__, self._dialect) + + +html = HTMLBuilder("html") +xhtml = HTMLBuilder("xhtml") + +# https://cgit.freedesktop.org/xdg/shared-mime-info/tree/freedesktop.org.xml.in +# https://www.iana.org/assignments/media-types/media-types.xhtml +# Types listed in the XDG mime info that have a charset in the IANA registration. +_charset_mimetypes = { + "application/ecmascript", + "application/javascript", + "application/sql", + "application/xml", + "application/xml-dtd", + "application/xml-external-parsed-entity", +} + + +def get_content_type(mimetype, charset): + """Returns the full content type string with charset for a mimetype. + + If the mimetype represents text, the charset parameter will be + appended, otherwise the mimetype is returned unchanged. + + :param mimetype: The mimetype to be used as content type. + :param charset: The charset to be appended for text mimetypes. + :return: The content type. + + .. verionchanged:: 0.15 + Any type that ends with ``+xml`` gets a charset, not just those + that start with ``application/``. Known text types such as + ``application/javascript`` are also given charsets. + """ + if ( + mimetype.startswith("text/") + or mimetype in _charset_mimetypes + or mimetype.endswith("+xml") + ): + mimetype += "; charset=" + charset + + return mimetype + + +def detect_utf_encoding(data): + """Detect which UTF encoding was used to encode the given bytes. + + The latest JSON standard (:rfc:`8259`) suggests that only UTF-8 is + accepted. Older documents allowed 8, 16, or 32. 16 and 32 can be big + or little endian. Some editors or libraries may prepend a BOM. + + :internal: + + :param data: Bytes in unknown UTF encoding. + :return: UTF encoding name + + .. versionadded:: 0.15 + """ + head = data[:4] + + if head[:3] == codecs.BOM_UTF8: + return "utf-8-sig" + + if b"\x00" not in head: + return "utf-8" + + if head in (codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE): + return "utf-32" + + if head[:2] in (codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE): + return "utf-16" + + if len(head) == 4: + if head[:3] == b"\x00\x00\x00": + return "utf-32-be" + + if head[::2] == b"\x00\x00": + return "utf-16-be" + + if head[1:] == b"\x00\x00\x00": + return "utf-32-le" + + if head[1::2] == b"\x00\x00": + return "utf-16-le" + + if len(head) == 2: + return "utf-16-be" if head.startswith(b"\x00") else "utf-16-le" + + return "utf-8" + + +def format_string(string, context): + """String-template format a string: + + >>> format_string('$foo and ${foo}s', dict(foo=42)) + '42 and 42s' + + This does not do any attribute lookup etc. For more advanced string + formattings have a look at the `werkzeug.template` module. + + :param string: the format string. + :param context: a dict with the variables to insert. + """ + + def lookup_arg(match): + x = context[match.group(1) or match.group(2)] + if not isinstance(x, string_types): + x = type(string)(x) + return x + + return _format_re.sub(lookup_arg, string) + + +def secure_filename(filename): + r"""Pass it a filename and it will return a secure version of it. This + filename can then safely be stored on a regular file system and passed + to :func:`os.path.join`. The filename returned is an ASCII only string + for maximum portability. + + On windows systems the function also makes sure that the file is not + named after one of the special device files. + + >>> secure_filename("My cool movie.mov") + 'My_cool_movie.mov' + >>> secure_filename("../../../etc/passwd") + 'etc_passwd' + >>> secure_filename(u'i contain cool \xfcml\xe4uts.txt') + 'i_contain_cool_umlauts.txt' + + The function might return an empty filename. It's your responsibility + to ensure that the filename is unique and that you abort or + generate a random filename if the function returned an empty one. + + .. versionadded:: 0.5 + + :param filename: the filename to secure + """ + if isinstance(filename, text_type): + from unicodedata import normalize + + filename = normalize("NFKD", filename).encode("ascii", "ignore") + if not PY2: + filename = filename.decode("ascii") + for sep in os.path.sep, os.path.altsep: + if sep: + filename = filename.replace(sep, " ") + filename = str(_filename_ascii_strip_re.sub("", "_".join(filename.split()))).strip( + "._" + ) + + # on nt a couple of special files are present in each folder. We + # have to ensure that the target file is not such a filename. In + # this case we prepend an underline + if ( + os.name == "nt" + and filename + and filename.split(".")[0].upper() in _windows_device_files + ): + filename = "_" + filename + + return filename + + +def escape(s, quote=None): + """Replace special characters "&", "<", ">" and (") to HTML-safe sequences. + + There is a special handling for `None` which escapes to an empty string. + + .. versionchanged:: 0.9 + `quote` is now implicitly on. + + :param s: the string to escape. + :param quote: ignored. + """ + if s is None: + return "" + elif hasattr(s, "__html__"): + return text_type(s.__html__()) + elif not isinstance(s, string_types): + s = text_type(s) + if quote is not None: + from warnings import warn + + warn( + "The 'quote' parameter is no longer used as of version 0.9" + " and will be removed in version 1.0.", + DeprecationWarning, + stacklevel=2, + ) + s = ( + s.replace("&", "&") + .replace("<", "<") + .replace(">", ">") + .replace('"', """) + ) + return s + + +def unescape(s): + """The reverse function of `escape`. This unescapes all the HTML + entities, not only the XML entities inserted by `escape`. + + :param s: the string to unescape. + """ + + def handle_match(m): + name = m.group(1) + if name in HTMLBuilder._entities: + return unichr(HTMLBuilder._entities[name]) + try: + if name[:2] in ("#x", "#X"): + return unichr(int(name[2:], 16)) + elif name.startswith("#"): + return unichr(int(name[1:])) + except ValueError: + pass + return u"" + + return _entity_re.sub(handle_match, s) + + +def redirect(location, code=302, Response=None): + """Returns a response object (a WSGI application) that, if called, + redirects the client to the target location. Supported codes are + 301, 302, 303, 305, 307, and 308. 300 is not supported because + it's not a real redirect and 304 because it's the answer for a + request with a request with defined If-Modified-Since headers. + + .. versionadded:: 0.6 + The location can now be a unicode string that is encoded using + the :func:`iri_to_uri` function. + + .. versionadded:: 0.10 + The class used for the Response object can now be passed in. + + :param location: the location the response should redirect to. + :param code: the redirect status code. defaults to 302. + :param class Response: a Response class to use when instantiating a + response. The default is :class:`werkzeug.wrappers.Response` if + unspecified. + """ + if Response is None: + from .wrappers import Response + + display_location = escape(location) + if isinstance(location, text_type): + # Safe conversion is necessary here as we might redirect + # to a broken URI scheme (for instance itms-services). + from .urls import iri_to_uri + + location = iri_to_uri(location, safe_conversion=True) + response = Response( + '\n' + "Redirecting...\n" + "

Redirecting...

\n" + "

You should be redirected automatically to target URL: " + '%s. If not click the link.' + % (escape(location), display_location), + code, + mimetype="text/html", + ) + response.headers["Location"] = location + return response + + +def append_slash_redirect(environ, code=301): + """Redirects to the same URL but with a slash appended. The behavior + of this function is undefined if the path ends with a slash already. + + :param environ: the WSGI environment for the request that triggers + the redirect. + :param code: the status code for the redirect. + """ + new_path = environ["PATH_INFO"].strip("/") + "/" + query_string = environ.get("QUERY_STRING") + if query_string: + new_path += "?" + query_string + return redirect(new_path, code) + + +def import_string(import_name, silent=False): + """Imports an object based on a string. This is useful if you want to + use import paths as endpoints or something similar. An import path can + be specified either in dotted notation (``xml.sax.saxutils.escape``) + or with a colon as object delimiter (``xml.sax.saxutils:escape``). + + If `silent` is True the return value will be `None` if the import fails. + + :param import_name: the dotted name for the object to import. + :param silent: if set to `True` import errors are ignored and + `None` is returned instead. + :return: imported object + """ + # force the import name to automatically convert to strings + # __import__ is not able to handle unicode strings in the fromlist + # if the module is a package + import_name = str(import_name).replace(":", ".") + try: + try: + __import__(import_name) + except ImportError: + if "." not in import_name: + raise + else: + return sys.modules[import_name] + + module_name, obj_name = import_name.rsplit(".", 1) + module = __import__(module_name, globals(), locals(), [obj_name]) + try: + return getattr(module, obj_name) + except AttributeError as e: + raise ImportError(e) + + except ImportError as e: + if not silent: + reraise( + ImportStringError, ImportStringError(import_name, e), sys.exc_info()[2] + ) + + +def find_modules(import_path, include_packages=False, recursive=False): + """Finds all the modules below a package. This can be useful to + automatically import all views / controllers so that their metaclasses / + function decorators have a chance to register themselves on the + application. + + Packages are not returned unless `include_packages` is `True`. This can + also recursively list modules but in that case it will import all the + packages to get the correct load path of that module. + + :param import_path: the dotted name for the package to find child modules. + :param include_packages: set to `True` if packages should be returned, too. + :param recursive: set to `True` if recursion should happen. + :return: generator + """ + module = import_string(import_path) + path = getattr(module, "__path__", None) + if path is None: + raise ValueError("%r is not a package" % import_path) + basename = module.__name__ + "." + for _importer, modname, ispkg in pkgutil.iter_modules(path): + modname = basename + modname + if ispkg: + if include_packages: + yield modname + if recursive: + for item in find_modules(modname, include_packages, True): + yield item + else: + yield modname + + +def validate_arguments(func, args, kwargs, drop_extra=True): + """Checks if the function accepts the arguments and keyword arguments. + Returns a new ``(args, kwargs)`` tuple that can safely be passed to + the function without causing a `TypeError` because the function signature + is incompatible. If `drop_extra` is set to `True` (which is the default) + any extra positional or keyword arguments are dropped automatically. + + The exception raised provides three attributes: + + `missing` + A set of argument names that the function expected but where + missing. + + `extra` + A dict of keyword arguments that the function can not handle but + where provided. + + `extra_positional` + A list of values that where given by positional argument but the + function cannot accept. + + This can be useful for decorators that forward user submitted data to + a view function:: + + from werkzeug.utils import ArgumentValidationError, validate_arguments + + def sanitize(f): + def proxy(request): + data = request.values.to_dict() + try: + args, kwargs = validate_arguments(f, (request,), data) + except ArgumentValidationError: + raise BadRequest('The browser failed to transmit all ' + 'the data expected.') + return f(*args, **kwargs) + return proxy + + :param func: the function the validation is performed against. + :param args: a tuple of positional arguments. + :param kwargs: a dict of keyword arguments. + :param drop_extra: set to `False` if you don't want extra arguments + to be silently dropped. + :return: tuple in the form ``(args, kwargs)``. + """ + parser = _parse_signature(func) + args, kwargs, missing, extra, extra_positional = parser(args, kwargs)[:5] + if missing: + raise ArgumentValidationError(tuple(missing)) + elif (extra or extra_positional) and not drop_extra: + raise ArgumentValidationError(None, extra, extra_positional) + return tuple(args), kwargs + + +def bind_arguments(func, args, kwargs): + """Bind the arguments provided into a dict. When passed a function, + a tuple of arguments and a dict of keyword arguments `bind_arguments` + returns a dict of names as the function would see it. This can be useful + to implement a cache decorator that uses the function arguments to build + the cache key based on the values of the arguments. + + :param func: the function the arguments should be bound for. + :param args: tuple of positional arguments. + :param kwargs: a dict of keyword arguments. + :return: a :class:`dict` of bound keyword arguments. + """ + ( + args, + kwargs, + missing, + extra, + extra_positional, + arg_spec, + vararg_var, + kwarg_var, + ) = _parse_signature(func)(args, kwargs) + values = {} + for (name, _has_default, _default), value in zip(arg_spec, args): + values[name] = value + if vararg_var is not None: + values[vararg_var] = tuple(extra_positional) + elif extra_positional: + raise TypeError("too many positional arguments") + if kwarg_var is not None: + multikw = set(extra) & set([x[0] for x in arg_spec]) + if multikw: + raise TypeError( + "got multiple values for keyword argument " + repr(next(iter(multikw))) + ) + values[kwarg_var] = extra + elif extra: + raise TypeError("got unexpected keyword argument " + repr(next(iter(extra)))) + return values + + +class ArgumentValidationError(ValueError): + + """Raised if :func:`validate_arguments` fails to validate""" + + def __init__(self, missing=None, extra=None, extra_positional=None): + self.missing = set(missing or ()) + self.extra = extra or {} + self.extra_positional = extra_positional or [] + ValueError.__init__( + self, + "function arguments invalid. (%d missing, %d additional)" + % (len(self.missing), len(self.extra) + len(self.extra_positional)), + ) + + +class ImportStringError(ImportError): + """Provides information about a failed :func:`import_string` attempt.""" + + #: String in dotted notation that failed to be imported. + import_name = None + #: Wrapped exception. + exception = None + + def __init__(self, import_name, exception): + self.import_name = import_name + self.exception = exception + + msg = ( + "import_string() failed for %r. Possible reasons are:\n\n" + "- missing __init__.py in a package;\n" + "- package or module path not included in sys.path;\n" + "- duplicated package or module name taking precedence in " + "sys.path;\n" + "- missing module, class, function or variable;\n\n" + "Debugged import:\n\n%s\n\n" + "Original exception:\n\n%s: %s" + ) + + name = "" + tracked = [] + for part in import_name.replace(":", ".").split("."): + name += (name and ".") + part + imported = import_string(name, silent=True) + if imported: + tracked.append((name, getattr(imported, "__file__", None))) + else: + track = ["- %r found in %r." % (n, i) for n, i in tracked] + track.append("- %r not found." % name) + msg = msg % ( + import_name, + "\n".join(track), + exception.__class__.__name__, + str(exception), + ) + break + + ImportError.__init__(self, msg) + + def __repr__(self): + return "<%s(%r, %r)>" % ( + self.__class__.__name__, + self.import_name, + self.exception, + ) + + +from werkzeug import _DeprecatedImportModule + +_DeprecatedImportModule( + __name__, + { + ".datastructures": [ + "CombinedMultiDict", + "EnvironHeaders", + "Headers", + "MultiDict", + ], + ".http": ["dump_cookie", "parse_cookie"], + }, + "Werkzeug 1.0", +) +del _DeprecatedImportModule diff --git a/uweb3/model.py b/uweb3/model.py index c3355cec..a4a289aa 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -17,7 +17,6 @@ class Error(Exception): """Superclass used for inheritance and external exception handling.""" - class DatabaseError(Error): """Superclass for errors returned by the database backend.""" @@ -30,14 +29,13 @@ class BadFieldError(DatabaseError): class AlreadyExistError(Error): """The resource already exists, and cannot be created twice.""" - class NotExistError(Error): """The requested or provided resource doesn't exist or isn't accessible.""" - class PermissionError(Error): """The entity has insufficient rights to access the resource.""" + class SettingsManager(object): def __init__(self, filename=None, executing_path=None): """Creates a ini file with the child class name @@ -47,10 +45,11 @@ def __init__(self, filename=None, executing_path=None): Name of the file without the extension """ self.options = None - self.FILENAME = f"{self.__class__.__name__[:1].lower() + self.__class__.__name__[1:]}.ini" if filename: self.FILENAME = f"{filename[:1].lower() + filename[1:]}.ini" + else: + self.FILENAME = f"{self.__class__.__name__[:1].lower() + self.__class__.__name__[1:]}.ini" self.FILE_LOCATION = os.path.join(executing_path, self.FILENAME) self.__CheckPermissions() @@ -63,6 +62,8 @@ def __init__(self, filename=None, executing_path=None): def __CheckPermissions(self): """Checks if SettingsManager can read/write to file.""" + if not os.path.isfile(self.FILE_LOCATION): + return True if not os.access(self.FILE_LOCATION, os.R_OK): raise PermissionError(f"SettingsManager missing permissions to read file: {self.FILE_LOCATION}") if not os.access(self.FILE_LOCATION, os.W_OK): @@ -88,10 +89,7 @@ def Create(self, section, key, value): raise ValueError("key already exists") self.config.set(section, key, value) - - with open(self.FILE_LOCATION, 'w') as configfile: - self.config.write(configfile) - self.Read() + self._Write(False) def Read(self): self.config.read(self.FILE_LOCATION) @@ -112,12 +110,9 @@ def Update(self, section, key, value): if not self.options.get(section): self.config.add_section(section) self.config.set(section, key, value) + self._Write() - with open(self.FILE_LOCATION, 'w') as configfile: - self.config.write(configfile) - self.Read() - - def Delete(self, section, key, delete_section=False): + def Delete(self, section, key=None): """Delete sections/keys from the INI file Be aware, deleting a section that is not empty will remove all keys from that given section @@ -125,43 +120,86 @@ def Delete(self, section, key, delete_section=False): Arguments: @ section: str Name of the section - @ key: str + @ key: None / str Name of the key you want to remove - % delete_section: boolean - If set to true it will delete the supplied section + If set to None (default) it will delete the supplied section Raises: configparser.NoSectionError """ - self.config.remove_option(section, key) - if delete_section: + if key: + self.config.remove_option(section, key) + if not key: self.config.remove_section(section) + self._Write() + + def _Write(self, reread=True): + """Internal function to store the current config to file""" with open(self.FILE_LOCATION, 'w') as configfile: self.config.write(configfile) - self.Read() + if reread: + return self.Read() + return True class SecureCookie(object): - def __init__(self): - self.req = self.secure_cookie_connection[0] - self.cookies = self.secure_cookie_connection[1] - self.cookie_salt = self.secure_cookie_connection[2] - self.cookiejar = self.__GetSessionCookies() - - def __GetSessionCookies(self): - cookiejar = {} - for key, value in self.cookies.items(): - if value: - isValid, value = self.__ValidateCookieHash(value) - if isValid: - cookiejar[key] = value - return cookiejar - - def Create(self, name, data, **attrs): + """The secureCookie class works just like other data abstraction classes, + except that it stores its data in client side cookies that are signed with a + server side secret to avoid tampering by the end-user. + + Subclass this class with your own class to create signed cookie objects. The + name for your class will reflect the name for the cookie in the cookie. + """ + + HASHTYPE = 'ripemd160' + _TABLE = None + _CONNECTOR = 'signedCookie' + + def __init__(self, connection): + """Create a new SecureCookie instance.""" + self.connection = connection + self.req, self.cookies, self.cookie_salt = self.connection + self.rawcookie = self.__GetCookie() + self.debug = self.connection.debug + if self.debug: + print(self.cookies) + + def __str__(self): + """Returns the cookie's value if it was valid and untampered with.""" + return str(self.rawcookie) + + @classmethod + def TableName(cls): + """Returns the 'database' table name for the SecureCookie class. + + If this is not explicitly defined by the class constant `_TABLE`, the return + value will be the class name with the first letter lowercased. + We stick to the same naming scheme as for more table like connectors even + though we use cookies instead of tables in this class. + """ + if cls._TABLE: + return cls._TABLE + name = cls.__name__ + return name[0].lower() + name[1:] + + def __GetCookie(self): + """Reads the request cookie, checks if it was signed correctly and return + the value, or returns False""" + name = self.TableName() + if name in self.cookies and self.cookies[name]: + isValid, value = self.__ValidateCookieHash(self.cookies[name]) + if isValid: + return value + if self.debug: + print('Secure cookie "%s" was tampered with and thus invalid.' % name) + if self.debug: + print('Secure cookie "%s" was not present.' % name) + return '' + + @classmethod + def Create(cls, connection, data, **attrs): """Creates a secure cookie Arguments: - @ name: str - Name of the cookie @ data: dict Needs to have a key called __name with value of how you want to name the 'table' % only_return_hash: boolean @@ -195,30 +233,23 @@ def Create(self, name, data, **attrs): Raises: ValueError: When cookie with name already exists """ - if not attrs.get('update') and self.cookiejar.get(name): - raise ValueError("Cookie with name already exists") - if attrs.get('update'): - self.cookiejar[name] = data - - hashed = self.__CreateCookieHash(data) - if not attrs.get('only_return_hash'): - #Delete all these settings to prevent them from injecting in a cookie - if attrs.get('update'): - del attrs['update'] - if attrs.get('only_return_hash'): - del attrs['only_return_hash'] - self.req.AddCookie(name, hashed, **attrs) - else: - return hashed + cls.connection = connection + cls.req, cls.cookies, cls.cookie_salt = connection + name = cls.TableName() + cls.rawcookie = data - def Update(self, name, data, **attrs): + hashed = cls.__CreateCookieHash(cls, data) + cls.cookies[name] = hashed + cls.req.AddCookie(name, hashed, **attrs) + return cls + + def Update(self, data, **attrs): """"Updates a secure cookie - Keep in mind that the actual cookie is updated on the next request. After calling - this method it will update the session attribute to the new value however. + Keep in mind that the actual cookie's value is avilable from the next + request. After calling this method it will update the cookie attribute to + the new value however. Arguments: - @ name: str - Name of the cookie @ data: dict Needs to have a key called __name with value of how you want to name the 'table' % only_return_hash: boolean @@ -252,30 +283,25 @@ def Update(self, name, data, **attrs): Raises: ValueError: When no cookie with given name found """ - if not self.cookiejar.get(name): - raise ValueError("No cookie with name `{}` found".format(name)) - - attrs['update'] = True - self.Create(name, data, **attrs) - + name = self.TableName() + if not self.rawcookie: + raise ValueError("No valid cookie with name `{}` found".format(name)) + self.rawcookie = data + self.req.AddCookie(name, self.__CreateCookieHash(data), **attrs) - def Delete(self, name): + def Delete(self): """Deletes cookie based on name The cookie is no longer in the session after calling this method - - Arguments: - % name: str - Deletes cookie by name """ + name = self.TableName() self.req.DeleteCookie(name) - if self.cookiejar.get(name): - self.cookiejar.pop(name) + self.rawcookie = None def __CreateCookieHash(self, data): hex_string = pickle.dumps(data).hex() hashed = (hex_string + self.cookie_salt).encode('utf-8') - h = hashlib.new('ripemd160') + h = hashlib.new(self.HASHTYPE) h.update(hashed) return '{}+{}'.format(h.hexdigest(), hex_string) @@ -293,10 +319,10 @@ def __ValidateCookieHash(self, cookie): except Exception: return (False, None) - if cookie != self.__CreateCookieHash(data): - return (False, None) + if cookie == self.__CreateCookieHash(data): + return (True, data) + return (False, None) - return (True, data) # Record classes have many methods, this is not an actual problem. # pylint: disable=R0904 @@ -449,7 +475,10 @@ def _PreCreate(self, _cursor): Typically you would verify values of the Record in this step, or transform the data for database-safe insertion. If the data is transformed here, this transformation should be reversed in `_PostCreate()`. + + Returning False from this method will halt the creation of the record. """ + return True def _PreSave(self, _cursor): """Hook that runs before saving (updating) a Record in the database. @@ -457,7 +486,10 @@ def _PreSave(self, _cursor): Typically you would verify values of the Record in this step, or transform the data for database-safe insertion. If the data is transformed here, this transformation should be reversed in `_PostSave()`. + + Returning False from this method will halt the updating of the record. """ + return True def _PostInit(self): """Hook that runs after initializing a Record instance. @@ -481,6 +513,15 @@ def _PostSave(self, _cursor): Any transforms that were performed on the data should be reversed here. """ + def _PreDelete(self): + """Hook that runs before deleting a Record in the database. + Returning False from this method will halt the deletion of the record. + """ + return True + + def _PostDelete(self): + """Hook that runs after deleting a Record in the database.""" + # ############################################################################ # Base record functionality methods, to be implemented by subclasses. # Some methods have a generic implementation, but may need customization, @@ -519,9 +560,12 @@ def Delete(self): For deleting an unloaded object, use the classmethod `DeletePrimary`. """ + if not self._PreDelete(): + return False self.DeletePrimary(self.connection, self.key) self._record.clear() self.clear() + self._PostDelete() @classmethod def FromPrimary(cls, connection, pkey_value): @@ -581,7 +625,6 @@ def _LoadAsForeign(cls, connection, relation_value, method=None): method = cls._LOAD_METHOD return getattr(cls, method)(connection, relation_value) - # ############################################################################ # Functions for tracking table and primary key values # @@ -599,7 +642,7 @@ def _DataRecord(self): For any Record object present, its primary key value (`Record.key`) is used. """ sql_record = {} - for key, value in super(BaseRecord, self).items(): + for key, value in super().items(): sql_record[key] = self._ValueOrPrimary(value) return sql_record @@ -607,7 +650,10 @@ def _DataRecord(self): def _ValueOrPrimary(value): """Returns the value, or its primary key value if it's a Record.""" while isinstance(value, BaseRecord): - value = value.key + if hasattr(value, '_RECORD_KEY') and value._RECORD_KEY: + value = value[value._RECORD_KEY] + else: + value = value.key return value @classmethod @@ -660,6 +706,8 @@ def key(self, value): class Record(BaseRecord): """Extensions to the Record abstraction for relational database use.""" _FOREIGN_RELATIONS = {} + _CONNECTOR = 'mysql' + SEARCHABLE_COLUMNS = [] # ############################################################################ # Methods enabling auto-loading @@ -768,15 +816,11 @@ def GetRecordClass(cls): if foreign_cls is None: return value - elif type(foreign_cls) is dict: + if type(foreign_cls) is dict: cls = GetRecordClass(foreign_cls['class']) loader = foreign_cls.get('loader') - value = cls._LoadAsForeign(self.connection, value, method=loader) - return value - else: - value = GetRecordClass(foreign_cls)._LoadAsForeign(self.connection, value) - self[field] = value - return value + return cls._LoadAsForeign(self.connection, value, method=loader) + return GetRecordClass(foreign_cls)._LoadAsForeign(self.connection, value) # ############################################################################ # Override basic dict methods so that autoload mechanisms function on them. @@ -830,7 +874,8 @@ def values(self): # @classmethod def _FromParent(cls, parent, relation_field=None, conditions=None, - limit=None, offset=None, order=None): + limit=None, offset=None, order=None, + yield_unlimited_total_first=False): """Returns all `cls` objects that are a child of the given parent. This utilized the parent's _Children method, with either this class' @@ -840,8 +885,8 @@ def _FromParent(cls, parent, relation_field=None, conditions=None, @ parent: Record The parent for who children should be found in this class % relation_field: str ~~ cls.TableName() - The fieldname in this class' table which relates to the parent's - primary key. If not given, parent.TableName() will be used. + The fieldname in this class' table which relates to the parent's primary + key. If not given, parent.TableName() will be used. % conditions: str / iterable ~~ None The extra condition(s) that should be applied when querying for records. % limit: int ~~ None @@ -851,10 +896,13 @@ def _FromParent(cls, parent, relation_field=None, conditions=None, Specifies the offset at which the yielded items should start. Combined with limit this enables proper pagination. % order: iterable of str/2-tuple - Defines the fields on which the output should be ordered. This should - be a list of strings or 2-tuples. The string or first item indicates - the field, the second argument defines descending order + Defines the fields on which the output should be ordered. This should be + a list of strings or 2-tuples. The string or first item indicates the + field, the second argument defines descending order (desc. if True). + % yield_unlimited_total_first: bool ~~ False + Instead of yielding only Record objects, the first item returned is the + number of results from the query if it had been executed without limit. """ if not isinstance(parent, Record): raise TypeError('parent argument should be a Record type.') @@ -866,12 +914,20 @@ def _FromParent(cls, parent, relation_field=None, conditions=None, qry_conditions.append(conditions) else: qry_conditions.extend(conditions) + + firstrow = yield_unlimited_total_first # set a flag to skip the linking of + # the first row to our parent, as that will be the full record cound instead + # of a record for record in cls.List(parent.connection, conditions=qry_conditions, - limit=limit, offset=offset, order=order): - record[relation_field] = parent.copy() + limit=limit, offset=offset, order=order, + yield_unlimited_total_first=yield_unlimited_total_first): + if not firstrow: + record[relation_field] = parent.copy() + firstrow = False yield record - def _Children(self, child_class, relation_field=None, conditions=None): + def _Children(self, child_class, relation_field=None, conditions=None, + limit=None, offset=None, order=None, yield_unlimited_total_first=False): """Returns all `child_class` objects related to this record. The table for the given `child_class` will be queried for all fields where @@ -883,16 +939,31 @@ def _Children(self, child_class, relation_field=None, conditions=None): @ child_class: type (Record subclass) The child class whose objects should be found. % relation_field: str ~~ self.TableName() - The fieldname in the `child_class` table which relates that table to - the table for this record. + The fieldname in the `child_class` table which relates that table to the + table for this record. % conditions: str / iterable ~~ The extra condition(s) that should be applied when querying for records. + % limit: int ~~ None + Specifies a maximum number of items to be yielded. The limit happens on + the database side, limiting the query results. + % offset: int ~~ None + Specifies the offset at which the yielded items should start. Combined + with limit this enables proper pagination. + % order: iterable of str/2-tuple + Defines the fields on which the output should be ordered. This should be + a list of strings or 2-tuples. The string or first item indicates the + field, the second argument defines descending order (desc. if True). + % yield_unlimited_total_first: bool ~~ False + Instead of yielding only Record objects, the first item returned is the + number of results from the query if it had been executed without limit. """ # Delegating to let child class handle its own querying. These are methods # for development, and are private only to prevent name collisions. # pylint: disable=W0212 return child_class._FromParent( - self, relation_field=relation_field, conditions=conditions) + self, relation_field=relation_field, conditions=conditions, + limit=limit, offset=offset, order=order, + yield_unlimited_total_first=yield_unlimited_total_first) def _DeleteChildren(self, child_class, relation_field=None): """Deletes all `child_class` objects related to this record. @@ -904,8 +975,8 @@ def _DeleteChildren(self, child_class, relation_field=None): @ child_class: type (Record subclass) The child class whose objects should be deleted. % relation_field: str ~~ self.TableName() - The fieldname in the `child_class` table which relates that table to - the table for this record. + The fieldname in the `child_class` table which relates that table to the + table for this record. """ relation_field = relation_field or self.TableName() with self.connection as cursor: @@ -915,7 +986,7 @@ def _DeleteChildren(self, child_class, relation_field=None): @classmethod def _PrimaryKeyCondition(cls, connection, value): - """Returns the MySQL primary key condition to be used.""" + """Returns the primary key condition to be used.""" if isinstance(cls._PRIMARY_KEY, tuple): if not isinstance(value, tuple): raise TypeError( @@ -925,9 +996,10 @@ def _PrimaryKeyCondition(cls, connection, value): values = tuple(map(cls._ValueOrPrimary, value)) return ' AND '.join('`%s` = %s' % (field, value) for field, value in zip(cls._PRIMARY_KEY, connection.EscapeValues(values))) - else: - return '`%s` = %s' % (cls._PRIMARY_KEY, - connection.EscapeValues(cls._ValueOrPrimary(value))) + return '`%s`.`%s` = %s' % ( + cls.TableName(), + cls._PRIMARY_KEY, + connection.EscapeValues(cls._ValueOrPrimary(value))) def _RecordCreate(self, cursor): """Inserts the record's current values in the database as a new record. @@ -935,8 +1007,8 @@ def _RecordCreate(self, cursor): Upon success, the record's primary key is set to the result's insertid """ try: - # Compound key case values = self._DataRecord() + # Compound key case if isinstance(self._PRIMARY_KEY, tuple): auto_inc_field = set(self._PRIMARY_KEY) - set(values) if auto_inc_field: @@ -950,13 +1022,13 @@ def _RecordCreate(self, cursor): except cursor.OperationalError as err_obj: if err_obj[0] == 1054: raise BadFieldError(err_obj[1]) - raise + raise DatabaseError(err_obj) def _RecordUpdate(self, cursor): """Updates the existing database entry with the record's current values. - The constraint with which the record is updated is the name and value of - the Record's primary key (`self._PRIMARY_KEY` and `self.key` resp.) + The constraint with which the record is updated is the name and value of the + Record's primary key (`self._PRIMARY_KEY` and `self.key` resp.) """ try: if isinstance(self._PRIMARY_KEY, tuple): @@ -964,13 +1036,15 @@ def _RecordUpdate(self, cursor): else: primary = self._record[self._PRIMARY_KEY] cursor.Update( - table=self.TableName(), values=self._Changes(), + table=self.TableName(), + values=self._Changes(), conditions=self._PrimaryKeyCondition(self.connection, primary)) except KeyError: raise Error('Cannot update record without pre-existing primary key.') except cursor.OperationalError as err_obj: if err_obj[0] == 1054: raise BadFieldError(err_obj[1]) + raise DatabaseError(err_obj) def _SaveForeign(self, cursor): """Recursively saves all nested Record instances.""" @@ -986,8 +1060,8 @@ def _SaveForeign(self, cursor): def _SaveSelf(self, cursor): """Updates the existing database entry with the record's current values. - The constraint with which the record is updated is the name and value of - the Record's primary key (`self._PRIMARY_KEY` and `self.key` resp.) + The constraint with which the record is updated is the name and value of the + Record's primary key (`self._PRIMARY_KEY` and `self.key` resp.) """ self._PreSave(cursor) difference = self._Changes() @@ -1029,7 +1103,8 @@ def FromPrimary(cls, connection, pkey_value): @classmethod def List(cls, connection, conditions=None, limit=None, offset=None, - order=None, yield_unlimited_total_first=False): + order=None, yield_unlimited_total_first=False, search=None, + tables=None, escape=True, fields=None): """Yields a Record object for every table entry. Arguments: @@ -1045,24 +1120,61 @@ def List(cls, connection, conditions=None, limit=None, offset=None, Specifies the offset at which the yielded items should start. Combined with limit this enables proper pagination. % order: iterable of str/2-tuple - Defines the fields on which the output should be ordered. This should - be a list of strings or 2-tuples. The string or first item indicates the + Defines the fields on which the output should be ordered. This should be + a list of strings or 2-tuples. The string or first item indicates the field, the second argument defines descending order (desc. if True). % yield_unlimited_total_first: bool ~~ False Instead of yielding only Record objects, the first item returned is the number of results from the query if it had been executed without limit. + % search: str + Specifies what string should be searched for in the default searchable + database columns. + % tables: str / iterable ~~ None + Specifies what tables should be searched queried + % escape: bool ~~ True + Are conditions escaped? + % fields: str / iterable ~~ * + Specifies what fields should be returned Yields: Record: Database record abstraction class. """ + if not tables: + tables = [cls.TableName()] + group = None + if fields is None: + fields = '%s.*' % cls.TableName() + if search: + group = '%s.%s' % (cls.TableName(), (cls.RecordKey() if getattr(cls, "RecordKey", None) else cls._PRIMARY_KEY)) + tables, searchconditions = cls._GetSearchQuery(connection, tables, search) + if conditions: + if type(conditions) == list: + conditions.extend(searchconditions) + else: + searchconditions.append(conditions) + conditions = searchconditions + else: + conditions = searchconditions + cacheable = False + if not offset or offset < 0: + offset = 0 + if hasattr(cls, '_addToCache'): + #TODO dont cache partial / multi-table objects + cacheable = True + connection.modelcache['_stats']['queries'].append('%s Record.List' % cls.TableName()) with connection as cursor: - records = cursor.Select( - table=cls.TableName(), conditions=conditions, limit=limit, - offset=offset, order=order, totalcount=yield_unlimited_total_first) + records = cursor.Select(fields=fields, + table=tables, conditions=conditions, + limit=limit, offset=offset, order=order, + totalcount=yield_unlimited_total_first, + escape=escape, group=group) if yield_unlimited_total_first: yield records.affected + records = [cls(connection, record) for record in list(records)] for record in records: - yield cls(connection, record) + yield record + if cacheable: + list(cls._cacheListPreseed(records)) # SQL Records have foreign relations, saving needs an extra argument for this. # pylint: disable=W0221 @@ -1086,6 +1198,63 @@ def Save(self, save_foreign=False): return self # pylint: enable=W0221 + @classmethod + def _GetSearchQuery(cls, connetion, tables, search): + """Extracts table information from the searchable columns list.""" + conditions = [] + like = 'like "%%%s%%"' % connection.EscapeValues(search.strip())[1:-1] + searchconditions = [] + thistable = cls.TableName() + for column in cls.SEARCHABLE_COLUMNS: + if '.' not in column: + searchconditions.append('`%s`.`%s` %s' % (thistable, column, like)) + continue + classname, column = column.split('.', 1) + othertable = cls._SUBTYPES[classname].TableName() + if (othertable != thistable and + othertable not in tables): + fkey = cls._FOREIGN_RELATIONS.get(classname, False) + key = table._PRIMARY_KEY + if fkey and fkey.get('LookupKey', False): + key = fkey.get('LookupKey') + elif getattr(table, "RecordKey", None): + key = table.RecordKey() + # add the cross table join + conditions.append('`%s`.`%s` = `%s`.`%s' % (thistable, + othertable, + othertable, + key)) + tables.append(othertable) + searchconditions.append('`%s`.`%s` %s' % (othertable, + column, like)) + + conditions.append('(%s)' % ' or '.join(searchconditions)) + return tables, conditions + + def __json__(self, complete=True, recursive=True): + """Returns a dictionary representation of the Record. + + Arguments: + % complete: bool ~~ False + Whether the foreign references on the object should all be resolved before + converting the Record to a dictionary. Either way, existing resolved + references will be represented as complete dictionaries. + + Returns: + dict: dictionary representation of the record. + """ + record_dict = {} + record = self if complete else dict(record) + for key, value in record.items(): + if isinstance(value, BaseRecord): + if complete and recursive: + record_dict[key] = value.__json__(complete=True, recursive=True) + else: + record_dict[key] = dict(value) + else: + record_dict[key] = value + return record_dict + class VersionedRecord(Record): """Basic class for database table/record abstraction.""" @@ -1140,33 +1309,105 @@ def FromIdentifier(cls, connection, identifier): return cls(connection, record[0]) @classmethod - def List(cls, connection, conditions=None): + def List(cls, connection, conditions=None, limit=None, offset=None, + order=None, yield_unlimited_total_first=False, search=None, + tables=None, escape=True, fields=None): """Yields the latest Record for each versioned entry in the table. Arguments: - @ connection: sqltalk.connection + @ connection: object Database connection to use. + % conditions: str / iterable ~~ None + Optional query portion that will be used to limit the list of results. + If multiple conditions are provided, they are joined on an 'AND' string. + % limit: int ~~ None + Specifies a maximum number of items to be yielded. The limit happens on + the database side, limiting the query results. + % offset: int ~~ None + Specifies the offset at which the yielded items should start. Combined + with limit this enables proper pagination. + % order: iterable of str/2-tuple + Defines the fields on which the output should be ordered. This should + be a list of strings or 2-tuples. The string or first item indicates the + field, the second argument defines descending order (desc. if True). + % yield_unlimited_total_first: bool ~~ False + Instead of yielding only Record objects, the first item returned is the + number of results from the query if it had been executed without limit. + % search: str + Specifies what string should be searched for in the default searchable + database columns. + % tables: str / iterable ~~ None + Specifies what tables should be searched queried + % escape: bool ~~ True + Are conditions escaped? + % fields: str / iterable ~~ * + Specifies what fields should be returned Yields: Record: The Record with the newest version for each versioned entry. """ - if isinstance(conditions, (list, tuple)): - conditions = ' AND '.join(conditions) + if not tables: + tables = [cls.TableName()] + if not fields: + fields = "%s.*" % cls.TableName() + else: + if fields != '*': + if type(fields) != str: + fields = ', '.join(connection.EscapeField(fields)) + else: + fields = connection.EscapeField(fields) + if search: + search = search.strip() + tables, searchconditions = cls._GetSearchQuery(connection, tables, search) + if conditions: + if type(conditions) == list: + conditions.extend(searchconditions) + else: + newconditions.append(conditions) + conditions = searchconditions + else: + conditions = searchconditions + field_escape = connection.EscapeField if escape else lambda x: x + if yield_unlimited_total_first and limit is not None: + totalcount = 'SQL_CALC_FOUND_ROWS' + else: + totalcount = '' + cacheable = False + if hasattr(cls, '_addToCache'): + #TODO dont cache partial / multi-table objects + cacheable = True + connection.modelcache['_stats']['queries'].append('%s VersionedRecord.List' % cls.TableName()) with connection as cursor: records = cursor.Execute(""" - SELECT `%(table)s`.* - FROM `%(table)s` + SELECT %(totalcount)s %(fields)s + FROM %(tables)s JOIN (SELECT MAX(`%(primary)s`) AS `max` FROM `%(table)s` GROUP BY `%(record_key)s`) AS `versions` ON (`%(table)s`.`%(primary)s` = `versions`.`max`) WHERE %(conditions)s - """ % {'primary': cls._PRIMARY_KEY, + %(order)s + %(limit)s + """ % {'totalcount': totalcount, + 'primary': cls._PRIMARY_KEY, 'record_key': cls.RecordKey(), + 'fields': fields, 'table': cls.TableName(), - 'conditions': conditions or '1'}) + 'tables': cursor._StringTable(tables, field_escape), + 'conditions': cursor._StringConditions(conditions, + field_escape), + 'order': cursor._StringOrder(order, field_escape), + 'limit': cursor._StringLimit(limit, offset)}) + if yield_unlimited_total_first and limit is not None: + with connection as cursor: + records.affected = cursor._Execute('SELECT FOUND_ROWS()')[0][0] + yield records.affected + # turn sqltalk rows into model + records = [cls(connection, record) for record in list(records)] for record in records: - yield cls(connection, record) + yield record + if cacheable: + list(cls._cacheListPreseed(records)) @classmethod def Versions(cls, connection, identifier, conditions='1'): @@ -1223,11 +1464,15 @@ def _PreSave(self, cursor): assume an AutoIncrement primary key field. """ super(VersionedRecord, self)._PreSave(cursor) - self.key = None + difference = self._Changes() + if difference: + self.key = None def _RecordUpdate(self, cursor): """All updates are handled as new inserts for the same Record Key.""" - self._RecordCreate(cursor) + difference = self._Changes() + if difference: + self._RecordCreate(cursor) # Pylint falsely believes this property is overwritten by its setter later on. # pylint: disable=E0202 @@ -1253,6 +1498,7 @@ def identifier(self, value): class MongoRecord(BaseRecord): """Abstraction of MongoDB collection records.""" _PRIMARY_KEY = '_id' + _CONNECTOR = 'mongo' @classmethod def Collection(cls, connection): @@ -1305,81 +1551,6 @@ def Save(self): def _StoreRecord(self): self.key = self.Collection(self.connection).save(self._DataRecord()) -class Smorgasbord(object): - """A connection tracker for uWeb3 Record classes. - - The idea is that you can set up a Smorgasbord with various different - connection types (Mongo and relational), and have the smorgasbord provide the - correct connection for the caller's needs. MongoReceord would be given the - MongoDB connection as expected, and all other users will be given a relational - database connection. - - This is highly beta and debugging is going to be at the very least interesting - because of __getattribute__ overriding that is necessary for this type of - behavior. - """ - CONNECTION_TYPES = 'mongo', 'relational' - - def __init__(self, connections=None): - self.connections = {} if connections is None else connections - - def AddConnection(self, connection, con_type): - """Adds a connection and its type to the Smorgasbord. - - The connection type should be one of the strings defined in the class - constant `CONNECTION_TYPES`. - """ - if con_type not in self.CONNECTION_TYPES: - raise ValueError('Unknown connection type %r' % con_type) - self.connections[con_type] = connection - - def RelevantConnection(self): - """Returns the relevant database connection dependant on the caller model. - - If the caller model cannot be determined, the 'relational' database - connection is returned as a fallback method. - """ - # Figure out caller type or instance - # pylint: disable=W0212 - caller_locals = sys._getframe(2).f_locals - # pylint: enable=W0212 - if 'self' in caller_locals: - caller_cls = type(caller_locals['self']) - else: - caller_cls = caller_locals.get('cls', type) - # Decide the type of connection to return for this caller - if issubclass(caller_cls, MongoRecord): - con_type = 'mongo' - else: - con_type = 'relational' # This is the default connection to return. - try: - return self.connections[con_type] - except KeyError: - raise TypeError('There is no connection for type %r' % con_type) - - def __enter__(self): - """Proxies the transaction to the underlying relevant connection. - - This is not quite as transparent a passthrough as using __getattribute__, - but it necessary due to performance optimizations done in Python2.7 - """ - return self.RelevantConnection().__enter__() - - def __exit__(self, *args): - """Proxies the transaction to the underlying relevant connection. - - This is not quite as transparent a passthrough as using __getattribute__, - but it necessary due to performance optimizations done in Python2.7 - """ - return self.RelevantConnection().__exit__(*args) - - def __getattribute__(self, attribute): - try: - # Pray to God we haven't overloaded anything from our connection classes. - return super(Smorgasbord, self).__getattribute__(attribute) - except AttributeError: - return getattr(self.RelevantConnection(), attribute) - def RecordTableNames(): """Yields Record subclasses that have been defined outside this module. @@ -1408,64 +1579,6 @@ def GetSubTypes(cls, seen=None): yield cls.TableName(), cls -def RecordToDict(record, complete=False, recursive=False): - """Returns a dictionary representation of the Record. - - Arguments: - @ record: Record - A record object that should be turned to a dictionary - % complete: bool ~~ False - Whether the foreign references on the object should all be resolved before - converting the Record to a dictionary. Either way, existing resolved - references will be represented as complete dictionaries. - % recursive: bool ~~ False - When this and `complete` are set True, foreign references will recursively - be resolved, resulting in the entire tree to be expanded before it is - converted to a dictionary. - - Returns: - dict: dictionary representation of the record. - """ - record_dict = {} - record = record if complete else dict(record) - for key, value in record.items(): - if isinstance(value, Record): - if complete and recursive: - record_dict[key] = RecordToDict(value, complete=True, recursive=True) - else: - record_dict[key] = dict(value) - else: - record_dict[key] = value - return record_dict - - -def MakeJson(record, complete=False, recursive=False, indent=None): - """Returns a JSON object string of the given `record`. - - The record may be a regular Python dictionary, in which case it will be - converted to JSON, with a few additional conversions for date and time types. - - If the record is a Record subclass, it is first passed through the - RecordToDict() function. The arguments `complete` and `recursive` function - similarly to the arguments on that function. - - Returns: - str: JSON representation of the given record dictionary. - """ - def _Encode(obj): - if isinstance(obj, datetime.datetime): - return obj.strftime('%F %T') - if isinstance(obj, datetime.date): - return obj.strftime('%F') - if isinstance(obj, datetime.time): - return obj.strftime('%T') - - if isinstance(record, BaseRecord): - record = RecordToDict(record, complete=complete, recursive=recursive) - return simplejson.dumps( - record, default=_Encode, sort_keys=True, indent=indent) - - import functools class CachedPage(object): @@ -1522,4 +1635,3 @@ def FromSignature(cls, connection, maxage, name, modulename, args, kwargs): return cls(connection, cache[0]) else: raise cls.NotExistError('No cached data found') - diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 05a8b0a1..3bbeae2e 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -10,12 +10,13 @@ import threading import time import hashlib +import glob from base64 import b64encode from pymysql import Error as pymysqlerr -import uweb3 -from uweb3.model import SecureCookie +import uweb3 +from ..connections import ConnectionManager from .. import response, templateparser RFC_1123_DATE = '%a, %d %b %Y %T GMT' @@ -141,7 +142,8 @@ def update(self, data=None, **kwargs): if kwargs: self.update(kwargs) -class XSRF(object): + +class XSRFToken(object): def __init__(self, seed, remote_addr): self.seed = seed self.remote_addr = remote_addr @@ -151,16 +153,13 @@ def generate_token(self): """Generate an XSRF token XSRF token is generated based on the unix timestamp from today, - a randomly generated seed and the IP addres from the user + a randomly generated seed and the IP addres from the request """ hashed = (str(self.unix_today) + self.seed + self.remote_addr).encode('utf-8') h = hashlib.new('ripemd160') h.update(hashed) return h.hexdigest() - def is_valid(self, supplied_token): - token = self.generate_token() - return token != supplied_token class Base(object): # Constant for persistent storage accross requests. This will be accessible @@ -168,41 +167,16 @@ class Base(object): PERSISTENT = CacheStorage() # Base paths for templates and public data. These are used in the PageMaker # classmethods that set up paths specific for that pagemaker. - PUBLIC_DIR = 'static' TEMPLATE_DIR = 'templates' def __init__(self): - self.storage = {} - self.messages = [] - self.extended_templates = {} self.persistent = self.PERSISTENT + # clean up any request tags in the template parser + if '__parser' in self.persistent: + self.persistent.Get('__parser').ClearRequestTags() - def Flash(self, message): - """Appends message to list, list element is vailable in the template under keyword messages - - Arguments: - @ message: str - Raises: - TypeError - """ - if not isinstance(message, str): - raise TypeError("Message is of incorrect type, Should be string.") - self.messages.append(message) - - def ExtendTemplate(self, title, template, **kwds): - """Extend the template on which this method is called. - - Arguments: - @ title: str - Name of the variable that you can access the extended template at - @ template: str - Name of the template that you want to extend - % **kwds: kwds - The keywords that you want to pass to the template. Works the same as self.parser.Parse('template.html', var=value) - """ - if self.extended_templates.get(title): - raise ValueError("There is already a template with this title") - self.extended_templates[title] = self.parser.Parse(template, **kwds) + def _PostInit(self): + pass @property def parser(self): @@ -215,11 +189,7 @@ def parser(self): if '__parser' not in self.persistent: self.persistent.Set('__parser', templateparser.Parser( self.options.get('templates', {}).get('path', self.TEMPLATE_DIR))) - parser = self.persistent.Get('__parser') - parser.messages = self.messages - parser.templates = self.extended_templates - parser.storage = self.storage - return parser + return self.persistent.Get('__parser') class WebsocketPageMaker(Base): @@ -238,21 +208,92 @@ def Connect(self, sid, env): print(f"User connected with SocketID {sid}: ") self.req = env + +class XSRFMixin(object): + """Provides XSRF protection by enabling setting xsrf token cookies, checking + them and setting a flag based on their value + + A seperate decorator can then be used to clear the POST/GET/PUT variables if + needed in specific pagemaker functions depending on that page's security + context. + """ + XSRFCOOKIE = 'xsrf' + XSRF_seed = str(os.urandom(32)) + + def validatexsrf(self): + """Sets the invalid_xsrf_token flag to true or false""" + self.invalid_xsrf_token = False + if self.req.method != 'GET': # GET calls will be ignored, but will set a cookie + self.invalid_xsrf_token = True + try: + user_supplied_xsrf_token = getattr(self, self.req.method.lower()).getfirst(self.XSRFCOOKIE) + self.invalid_xsrf_token = (self.cookies.get(self.XSRFCOOKIE) != user_supplied_xsrf_token) + except Exception: + # any error in looking up the cookie of the supplied post vars will result in a invalid xsrf token flag + pass + # If no cookie is present, set it. + self._Set_XSRF_cookie() + + def _Set_XSRF_cookie(self): + """This creates a new XSRF token for this client, which is IP bound, and + stores it in a cookie. + """ + xsrf_cookie = self.cookies.get(self.XSRFCOOKIE, False) + if not xsrf_cookie: + xsrf_cookie = XSRFToken(self.XSRF_seed, self.req.env['REAL_REMOTE_ADDR']).generate_token() + self.req.AddCookie(self.XSRFCOOKIE, xsrf_cookie, path="/", httponly=True) + return xsrf_cookie + + def XSRFInvalidToken(self): + """Returns an error message regarding an incorrect XSRF token.""" + errorpage = templateparser.FileTemplate(os.path.join( + os.path.dirname(__file__), 'http_403.html')) + error = """Your browser did not send us the correct token, any token at all, or a timed out token. + Because of this we cannot allow you to perform this action at this time. Please refresh the previous page and try again.""" + + return uweb3.Response(content=errorpage.Parse(error=error), + httpcode=403, headers=self.req.response.headers) + + def _Get_XSRF(self): + """Easy access to the XSRF token""" + try: + return self.cookies[self.XSRFCOOKIE] + except KeyError: + return self._Set_XSRF_cookie() + + +class LoginMixin(object): + """This mixin provides a few methods that help with handling logins, sessions + and related database/cookie interaction""" + + def _ReadSession(self): + return NotImplemented + + @property + def user(self): + """Returns the current user""" + if not hasattr(self, '_user') or not self._user: + try: + self._user = self._ReadSession() + except ValueError: + self._user = False + return self._user + + class BasePageMaker(Base): """Provides the base pagemaker methods for all the html generators.""" _registery = [] # Default Static() handler cache durations, per MIMEtype, in days - CACHE_DURATION = MimeTypeDict({'text': 7, 'image': 30, 'application': 7}) + PUBLIC_DIR = 'static' + CACHE_DURATION = MimeTypeDict({'text': 7, 'image': 30, 'application': 7, + 'text/css': 7}) def __init__(self, req, config=None, - secure_cookie_secret=None, - executing_path=None, - XSRF_seed=None): + executing_path=None): """sets up the template parser and database connections. - Handles setting the XSRF flag for each incoming request. Arguments: @ req: request.Request @@ -260,110 +301,61 @@ def __init__(self, % config: dict ~~ None Configuration for the pagemaker, with database connection information and other settings. This will be available through `self.options`. - % secure_cookie_secret: Randomly generated os.urandom(32) byte string - This is used as a secret for the SecureCookie class % executing_path: str/path This is the path to the uWeb3 routing file. - % XSRF_seed: Randomly generated os.urandom(32) byte string - This is used as a secret for the XSRF hash in the XSRF class. """ super(BasePageMaker, self).__init__() self.__SetupPaths(executing_path) self.req = req self.cookies = req.vars['cookie'] self.get = req.vars['get'] - self.post = req.vars['post'] - self.put = req.vars['put'] - self.delete = req.vars['delete'] - self.options = config or {} - self.secure_cookie_connection = (self.req, self.cookies, secure_cookie_secret) - self.set_invalid_xsrf_token_flag(XSRF_seed) - - def set_invalid_xsrf_token_flag(self, XSRF_seed): - """Sets the invalid_xsrf_token flag to true or false""" - self.invalid_xsrf_token = False - if self.req.method != 'GET': - user_supplied_xsrf_token = getattr(self, self.req.method.lower()).get('xsrf') - xsrf = XSRF(XSRF_seed, self.req.env['REAL_REMOTE_ADDR']) - self.invalid_xsrf_token = xsrf.is_valid(user_supplied_xsrf_token) - #First we try to validate the token, then we check if the user has an xsrf cookie - self._Set_XSRF_cookie(XSRF_seed) - - - def _Set_XSRF_cookie(self, XSRF_seed): - """Checks if XSRF is enabled in the config and handles accordingly - - If XSRF is enabled it will check if there is an XSRF cookie, if not create one. - If XSRF is disabled nothing will happen - """ - if self.options.get('development'): - xsrf_enabled = self.options['development'].get('xsrf') - if xsrf_enabled == "True": - xsrf_cookie = self.cookies.get('xsrf') - if self.invalid_xsrf_token: - self.req.AddCookie("xsrf", XSRF(XSRF_seed, self.req.env['REAL_REMOTE_ADDR']).generate_token()) - return - if not xsrf_cookie: - self.req.AddCookie("xsrf", XSRF(XSRF_seed, self.req.env['REAL_REMOTE_ADDR']).generate_token()) - return - - def _PostRequest(self, response): - if response.status == '500 Internal Server Error': - if not hasattr(self, 'connection_error'): #this is set when we try and create a connection but it failed - #TODO: This requires some testing - print("ATTEMPTING TO ROLLBACK DATABASE") - try: - with self.connection as cursor: - cursor.Execute("ROLLBACK") - except Exception: - if hasattr(self, 'connection'): - if self.connection.open: - self.connection.close() - self.persistent.Del("__mysql") - self.connection_error = False - return response - - def XSRFInvalidToken(self, command): - """Returns an error message regarding an incorrect XSRF token.""" - page_data = self.parser.Parse('403.html', error=command) - return uweb3.Response(content=page_data, httpcode=403, headers=self.req.response.headers) + self.post = req.vars['post'] if 'post' in req.vars else {} + self.put = req.vars['put'] if 'put' in req.vars else {} + self.delete = req.vars['delete'] if 'delete' in req.vars else {} + self.config = config or None + self.options = config.options if config else {} + self.debug = DebuggerMixin in self.__class__.__mro__ + try: + self.connection = self.persistent.Get('connection') + except KeyError: + self.persistent.Set('connection', ConnectionManager(self.config, self.options, self.debug)) + self.connection = self.persistent.Get('connection') @classmethod - def LoadModules(cls, default_routes='routes', excluded_files=('__init__', '.pyc')): + def LoadModules(cls, routes='routes/*.py'): """Loops over all .py files apart from some exceptions in target directory Looks for classes that contain pagemaker Arguments: % default_routes: str - Path to the directory where you want to store your routes. Defaults to routes. - % excluded_files: tuple(str) - Extension name of the files you want to exclude. Default excluded files are __init__ and .pyc. + Location to your route files. Defaults to routes/*.py + Supports glob style syntax, non recursive. """ bases = [] - routes = os.path.join(os.getcwd(), default_routes) - for path, dirnames, filenames in os.walk(routes): - for filename in filenames: - name, ext = os.path.splitext(filename) - if name not in excluded_files and ext not in excluded_files: - f = os.path.relpath(os.path.join(os.getcwd(), default_routes, filename[:-3])).replace('/', '.') - example_data = pyclbr.readmodule_ex(f) - for name, data in example_data.items(): - if hasattr(data, 'super'): - if 'PageMaker' in data.super[0]: - module = __import__(f, fromlist=[name]) - bases.append(getattr(module, name)) + for file in glob.glob(routes): + module = os.path.relpath(os.path.join(os.getcwd(), file[:-3])).replace('/', '.') + classlist = pyclbr.readmodule_ex(module) + for name, data in classlist.items(): + if hasattr(data, 'super'): + if 'PageMaker' in data.super[0]: + module = __import__(f, fromlist=[name]) + bases.append(getattr(module, name)) return bases def _PostInit(self): """Method that gets called for derived classes of BasePageMaker.""" + def _ConnectionRollback(self): + """Roll back all connections, this method can be overwritten by the user""" + self.connection.Rollback() + @classmethod def __SetupPaths(cls, executing_path): """This sets up the correct paths for the PageMaker subclasses. From the passed in `cls`, it retrieves the filename. Of that path, the - directory is used as the working directory. Then, the module constants - PUBLIC_DIR and TEMPLATE_DIR are used to define class constants from. + directory is used as the working directory. Then, the module constant + TEMPLATE_DIR is used to define class constants from. """ # Unfortunately, mod_python does not always support retrieving the caller # filename using sys.modules. In those cases we need to query the stack. @@ -387,6 +379,58 @@ def __SetupPaths(cls, executing_path): cls.PUBLIC_DIR = os.path.join(cls_dir, cls.PUBLIC_DIR) cls.TEMPLATE_DIR = os.path.join(cls_dir, cls.TEMPLATE_DIR) + def Static(self, rel_path): + """Provides a handler for static content. + + The requested `path` is truncated against a root (removing any uplevels), + and then added to the working dir + PUBLIC_DIR. If the request file exists, + then the requested file is retrieved, its mimetype guessed, and returned + to the client performing the request. + + Should the requested file not exist, a 404 page is returned instead. + + Arguments: + @ rel_path: str + The filename relative to the working directory of the webserver. + + Returns: + Page: contains the content and mimetype of the requested file, or a 404 + page if the file was not available on the local path. + """ + rel_path = os.path.abspath(os.path.join(os.path.sep, rel_path))[1:] + abs_path = os.path.join(self.PUBLIC_DIR, rel_path) + try: + content_type, _encoding = mimetypes.guess_type(abs_path) + if not content_type: + content_type = 'text/plain' + binary = False + if not content_type.startswith('text/'): + binary = True + with open(abs_path, 'rb' if binary else 'r') as staticfile: + mtime = os.path.getmtime(abs_path) + length = os.path.getsize(abs_path) + cache_days = self.CACHE_DURATION.get(content_type, 0) + expires = datetime.datetime.utcnow() + datetime.timedelta(cache_days) + return response.Response(content=staticfile.read(), + content_type=content_type, + headers={'Expires': expires.strftime(RFC_1123_DATE), + 'cache-control': 'max-age=%d' % + (cache_days*24*60*60), + 'last-modified': time.ctime(mtime), + 'content-length': length}) + except IOError: + return self._StaticNotFound(rel_path) + + def _StaticNotFound(self, _path): + message = 'This is not the path you\'re looking for. No such file %r' % ( + self.req.env['PATH_INFO']) + return response.Response(message, content_type='text/plain', httpcode=404) + + def _NotFound(self, _path): + message = 'This is not the path you\'re looking for. No such path %r' % ( + self.req.env['PATH_INFO']) + return response.Response(message, content_type='text/html', httpcode=404) + def InternalServerError(self, exc_type, exc_value, traceback): """Returns a plain text notification about an internal server error.""" error = 'INTERNAL SERVER ERROR (HTTP 500) DURING PROCESSING OF %r' % ( @@ -401,25 +445,9 @@ def Reload(): """Raises `ReloadModules`, telling the Handler() to reload its pageclass.""" raise ReloadModules('Reloading ... ') - def _GetXSRF(self): - if 'xsrf' in self.cookies: - return self.cookies['xsrf'] - return None - - def CommonBlocks(self, title, page_id=None, scripts=None): - """Returns a dictionary with the header and footer in it.""" - if not page_id: - page_id = title.replace(' ', '_').lower() - - return {'header': self.parser.Parse( - 'header.html', title=title, page_id=page_id - ), - 'footer': self.parser.Parse( - 'footer.html', year=time.strftime('%Y'), - page_id=page_id, scripts=scripts - ), - 'page_id': page_id, - } + def _PostRequest(self): + """Method that gets called after each request""" + self.connection.PostRequest() class DebuggerMixin(object): @@ -429,8 +457,7 @@ class DebuggerMixin(object): lacks interactive functions. """ CACHE_DURATION = MimeTypeDict({}) - ERROR_TEMPLATE = templateparser.FileTemplate(os.path.join( - os.path.dirname(__file__), 'http_500.html')) + ERROR_TEMPLATE = 'http_500.html' def _ParseStackFrames(self, stack): """Generates list items for traceback information. @@ -491,9 +518,12 @@ def InternalServerError(self, exc_type, exc_value, traceback): 'error_for_error': False, 'exc': {'type': exc_type, 'value': exc_value, 'traceback': self._ParseStackFrames(traceback)}} + + error_template = templateparser.FileTemplate(os.path.join( + os.path.dirname(__file__), self.ERROR_TEMPLATE)) try: return response.Response( - self.ERROR_TEMPLATE.Parse(**exception_data), httpcode=500) + error_template.Parse(**exception_data), httpcode=500) except Exception: exc_type, exc_value, traceback = sys.exc_info() self.req.registry.logger.critical( @@ -504,134 +534,77 @@ def InternalServerError(self, exc_type, exc_value, traceback): exception_data['exc'] = {'type': exc_type, 'value': exc_value, 'traceback': self._ParseStackFrames(traceback)} return response.Response( - self.ERROR_TEMPLATE.Parse(**exception_data), httpcode=500) + error_template.Parse(**exception_data), httpcode=500) -class MongoMixin(object): - """Adds MongoDB support to PageMaker.""" - @property - def mongo(self): - """Returns a MongoDB database connection.""" - if '__mongo' not in self.persistent: - import pymongo - mongo_config = self.options.get('mongo', {}) - connection = pymongo.connection.Connection( - host=mongo_config.get('host'), - port=mongo_config.get('port')) - if 'database' in mongo_config: - self.persistent.Set('__mongo', connection[mongo_config['database']]) - else: - self.persistent.Set('__mongo', connection) - return self.persistent.Get('__mongo') - - -class SqlAlchemyMixin(object): - """Adds MysqlAlchemy connection to PageMaker.""" - @property - def engine(self): - if '__sql_alchemy' not in self.persistent: - from sqlalchemy import create_engine - mysql_config = self.options['mysql'] - engine = create_engine('mysql://{username}:{password}@{host}/{database}'.format( - username=mysql_config.get('user'), - password=mysql_config.get('password'), - host=mysql_config.get('host', 'localhost'), - database=mysql_config.get('database')), pool_size=5, max_overflow=0) - self.persistent.Set('__sql_alchemy', engine) - return self.persistent.Get('__sql_alchemy') +class CSPMixin(object): + """Provides CSP header output. - @property - def session(self): - from sqlalchemy.orm import sessionmaker - Session = sessionmaker() - Session.configure(bind=self.engine, expire_on_commit=False) - return Session() - -class MysqlMixin(object): - """Adds MySQL support to PageMaker.""" - @property - def connection(self): - """Returns a MySQL database connection.""" - try: - if '__mysql' not in self.persistent: - from libs.sqltalk import mysql - mysql_config = self.options['mysql'] - self.persistent.Set('__mysql', mysql.Connect( - host=mysql_config.get('host', 'localhost'), - user=mysql_config.get('user'), - passwd=mysql_config.get('password'), - db=mysql_config.get('database'), - charset=mysql_config.get('charset', 'utf8'), - debug=DebuggerMixin in self.__class__.__mro__)) - return self.persistent.Get('__mysql') - except Exception as e: - self.connection_error = True - raise e - - - -class SqliteMixin(object): - """Adds SQLite support to PageMaker.""" - @property - def connection(self): - """Returns an SQLite database connection.""" - if '__sqlite' not in self.persistent: - from libs.sqltalk import sqlite - self.persistent.Set('__sqlite', sqlite.Connect( - self.options['sqlite']['database'])) - return self.persistent.Get('__sqlite') - -class SmorgasbordMixin(object): - """Provides multiple-database connectivity. - - This enables a developer to use a single 'connection' property (`bord`) which - can be used for regular relation database and MongoDB access. The caller will - be given the relation database connection, unless Smorgasbord is aware of - the caller's needs for another database connection. + https://content-security-policy.com/ """ - class Connections(dict): - """Connection autoloading class for Smorgasbord.""" - def __init__(self, pagemaker): - super(SmorgasbordMixin.Connections, self).__init__() - self.pagemaker = pagemaker - - def __getitem__(self, key): - """Returns the requested database connection type. - - If the database connection type isn't locally available, it is retrieved - using one of the _Load* methods. - """ - try: - return super(SmorgasbordMixin.Connections, self).__getitem__(key) - except KeyError: - return self.setdefault(key, getattr(self, '_Load%s' % key.title())()) - - def _LoadMongo(self): - """Returns the PageMaker's MongoDB connection.""" - return self.pagemaker.mongo + _csp = { + "default-src": ("'none'",), + "object-src": ("'none'",), + "script-src": ("'none'",), + "style-src": ("'none'",), + "form-action": ("'none'",), + "connect-src": ("'none'",), + "img-src": ("'none'",), + "font-src": ("'none'",), + "frame-ancestors": ("'none'",), + "base-uri": ("'none'",) + } + + def _SetCsp(self, resourcetype="default-src", urls=("'self'", ), append=True): + """Add a new CSP url to the csp headers for the given resourcetype. + + resourcetype is any of the CSP resource types as defined in: + https://content-security-policy.com/#directive + defaults to: default-src + + urls should be one or more of: + https://content-security-policy.com/#source_list + default to 'self' + string or tuple/list is allowed + + By default this appends to the already present list of sources for the given + resourcetype - def _LoadRelational(self): - """Returns the PageMaker's relational database connection.""" - return self.pagemaker.connectionPageMaker - - @property - def bord(self): - """Returns a Smorgasbord of autoloading database connections.""" - if '__bord' not in self.persistent: - from .. import model - self.persistent.Set('__bord', model.Smorgasbord( - connections=SmorgasbordMixin.Connections(self))) - return self.persistent.Get('__bord') + """ + if isinstance(urls, str): + urls = [urls, ] + else: + urls = list(urls) if type(urls) == tuple else urls + if resourcetype not in self._csp: + self._csp[resourcetype] = [] + if self._csp[resourcetype] == "'none'" or not append: + self._csp[resourcetype] = urls + return + self._csp[resourcetype].extend(urls) + + def _CSPFromConfig(self, config): + """sets the CSP headers from a Dictionary + Dict keys should be resourcetypes, values should be lists of urls + + resourcetype is any of the CSP resource types as defined in: + https://content-security-policy.com/#directive + + urls are in the form: + https://content-security-policy.com/#source_list + """ + self._csp = config + def _CSPheaders(self): + """Adds the constructed CSP header to the request""" + csp = '; '.join(["%s %s" % (key, ' '.join(value)) for key, value in self._csp.items()]) + self.req.AddHeader('Content-Security-Policy', csp) # ############################################################################## # Classes for public use (wildcard import) # -class SqAlchemyPageMaker(SqlAlchemyMixin, BasePageMaker): - """The basic PageMaker class, providing MySQL support.""" +class PageMaker(XSRFMixin, BasePageMaker): + """The basic PageMaker class, providing XSRF support.""" -class PageMaker(MysqlMixin, BasePageMaker): - """The basic PageMaker class, providing MySQL support.""" class DebuggingPageMaker(DebuggerMixin, PageMaker): """The same basic PageMaker, with added debugging on HTTP 500.""" diff --git a/uweb3/pagemaker/decorators.py b/uweb3/pagemaker/decorators.py index 81adf5cc..495ecc0a 100644 --- a/uweb3/pagemaker/decorators.py +++ b/uweb3/pagemaker/decorators.py @@ -8,17 +8,15 @@ import simplejson import time -from pymysql import Error - import uweb3 from uweb3 import model -from uweb3.request import PostDictionary +from uweb3.request import IndexedFieldStorage def loggedin(f): """Decorator that checks if the user requesting the page is logged in based on set cookie.""" def wrapper(*args, **kwargs): if not args[0].user: - return args[0].req.Redirect('/login', httpcode=303) + return args[0].RequestLogin() return f(*args, **kwargs) return wrapper @@ -27,21 +25,21 @@ def checkxsrf(f): The function will compare the XSRF in the user's cookie and in the (post) request. Make sure to have xsrf_enabled = True in the config.ini """ - def _clear_form_data(*args): - method = args[0].req.method.lower() - #Set an attribute in the pagemaker that holds the form data on an invalid XSRF validation - args[0].invalid_form_data = getattr(args[0], method) - #Remove the form data from the PageMaker - setattr(args[0], method, PostDictionary()) - #Remove the form data from the Request class - args[0].req.vars[method] = PostDictionary() - return args + def _clear_form_data(pagemaker): + method = pagemaker.req.method.lower() + # Set an attribute in the pagemaker that holds the form data on an invalid XSRF validation + pagemaker.invalid_xsrf_data = getattr(pagemaker, method) + # Remove the form data from the PageMaker + setattr(pagemaker, method, IndexedFieldStorage()) + # Remove the form data from the Request class + pagemaker.req.vars[method] = IndexedFieldStorage() + return pagemaker def wrapper(*args, **kwargs): if args[0].req.method != "GET": if args[0].invalid_xsrf_token: - args = _clear_form_data(*args) - return args[0].XSRFInvalidToken('XSRF token is invalid or missing') + _clear_form_data(args[0]) + return args[0].XSRFInvalidToken() return f(*args, **kwargs) return wrapper @@ -132,12 +130,36 @@ def wrapper(*args, **kwargs): cache.Save() if verbose: data += '' - except Error: #This is probably a pymysql Error. or db collision, whilst unfortunate, we wont break the page on this + except Exception: #This is probably a pymysql Error. or db collision, whilst unfortunate, we wont break the page on this pass return data return wrapper return cache_decorator +def ContentType(content_type): + """Decorator that wraps and returns sets the contentType.""" + def content_type_decorator(f): + def wrapper(*args, **kwargs): + pageresult = f(*args, **kwargs) or {} + if not isinstance(pageresult, uweb3.Response): + return uweb3.Response(pageresult, + content_type=content_type) + if isinstance(pageresult, uweb3.Response): + pageresult.content_type = content_type + args[0].req.content_type = content_type + return pageresult + return wrapper + return content_type_decorator + +def CSP(resourcetype, urls, append=True): + """Decorator that injects a new CSP allowed source into the current csp output.""" + def csp_decorator(f): + def wrapper(*args, **kwargs): + args[0]._SetCsp(resourcetype, urls, append) + return f(*args, **kwargs) or {} + return wrapper + return csp_decorator + def TemplateParser(template, *t_args, **t_kwargs): """Decorator that wraps and returns the output. @@ -148,7 +170,6 @@ def template_decorator(f): def wrapper(*args, **kwargs): pageresult = f(*args, **kwargs) or {} if not isinstance(pageresult, (str, uweb3.Response, uweb3.Redirect)): - pageresult.update(args[0].CommonBlocks(*t_args, **t_kwargs)) return args[0].parser.Parse(template, **pageresult) return pageresult return wrapper diff --git a/uweb3/pagemaker/http_403.html b/uweb3/pagemaker/http_403.html new file mode 100644 index 00000000..c96a76dd --- /dev/null +++ b/uweb3/pagemaker/http_403.html @@ -0,0 +1,27 @@ + + + + Your session has timed our. µWeb3 403 Error. + + + + +

+

Error, your session has timed out (HTTP 403)

+
+
+
+ {{if [error] }} +

[error]

+ {{ endif }} +
+
+ + diff --git a/uweb3/pagemaker/http_500.html b/uweb3/pagemaker/http_500.html index f7ea4925..c48a949c 100644 --- a/uweb3/pagemaker/http_500.html +++ b/uweb3/pagemaker/http_500.html @@ -128,7 +128,7 @@

Traceback (most recent call first)

Frame locals {{ for name, value in [frame:locals|items|sorted] }} - [name][value] + [name]{{ if type([value]) == str}}"[value]"{{else}}[value]{{endif}} {{ endfor }} diff --git a/uweb3/request.py b/uweb3/request.py index d93c6773..70c80201 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -1,30 +1,26 @@ -#!/usr/bin/python2.6 -"""uWeb3 request module.""" +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +"""µWeb3 request module.""" # Standard modules import cgi import sys import urllib +import io from cgi import parse_qs -try: - # python 2 - import cStringIO as stringIO - import Cookie as cookie -except ImportError: - # python 3 - import io as stringIO - import http.cookies as cookie +import io as stringIO +import http.cookies as cookie import re import json + # uWeb modules -from uweb3 import response -from werkzeug.formparser import parse_form_data -from werkzeug.datastructures import MultiDict +from . import response -class CookieToBigError(Exception): +class CookieTooBigError(Exception): """Error class for cookie when size is bigger than 4096 bytes""" + class Cookie(cookie.SimpleCookie): """Cookie class that uses the most specific value for a cookie name. @@ -53,34 +49,6 @@ def _BaseCookie__set(self, key, real_value, coded_value): dict.__setitem__(self, key, morsel) -class PostDictionary(MultiDict): - """ """ - #TODO: Add basic uweb functions - - def getfirst(self, key, default=None): - """Returns the first item out of the list from the given key - - Arguments: - @ key: str - % default: any - """ - items = dict(self.lists()) - try: - return items[key][0] - except KeyError: - return default - - def getlist(self, key): - """Returns a list with all values that were given for the requested key. - - N.B. If the given key does not exist, an empty list is returned. - """ - items = dict(self.lists()) - try: - return items[key] - except KeyError: - return [] - class Request(object): def __init__(self, env, registry): self.env = env @@ -92,30 +60,21 @@ def __init__(self, env, registry): self.method = self.env['REQUEST_METHOD'] self.vars = {'cookie': dict((name, value.value) for name, value in Cookie(self.env.get('HTTP_COOKIE')).items()), - 'get': PostDictionary(cgi.parse_qs(self.env.get('QUERY_STRING'))), - 'post': PostDictionary(), - 'put': PostDictionary(), - 'delete': PostDictionary(), - } + 'get': QueryArgsDict(cgi.parse_qs(self.env['QUERY_STRING']))} self.env['host'] = self.headers.get('Host', '') - if self.method == 'POST': - stream, form, files = parse_form_data(self.env) + if self.method in ('POST', 'PUT', 'DELETE'): + request_body_size = 0 + try: + request_body_size = int(self.env.get('CONTENT_LENGTH', 0)) + except Exception: + pass + request_payload = self.env['wsgi.input'].read(request_body_size) + self.input = request_payload if self.env['CONTENT_TYPE'] == 'application/json': - try: - request_body_size = int(self.env.get('CONTENT_LENGTH', 0)) - except (ValueError): - request_body_size = 0 - request_body = self.env['wsgi.input'].read(request_body_size) - data = json.loads(request_body) - self.vars['post'] = PostDictionary(MultiDict(data)) + self.vars[self.method.lower()] = json.loads(request_payload) else: - self.vars['post'] = PostDictionary(form) - for f in files: - self.vars['post'][f] = files.get(f) - else: - if self.method in ('PUT', 'DELETE'): - stream, form, files = parse_form_data(self.env) - self.vars[self.method.lower()] = PostDictionary(form) + self.vars[self.method.lower()] = IndexedFieldStorage(stringIO.StringIO(request_payload.decode("utf-8")), + environ={'REQUEST_METHOD': 'POST'}) @property def path(self): @@ -179,7 +138,7 @@ def AddCookie(self, key, value, **attrs): """ if isinstance(value, (str)): if len(value.encode('utf-8')) >= 4096: - raise CookieToBigError("Cookie is larger than 4096 bytes and wont be set") + raise CookieTooBigError("Cookie is larger than 4096 bytes and wont be set") new_cookie = Cookie({key: value}) if 'max_age' in attrs: @@ -238,38 +197,41 @@ def read_urlencoded(self): self.list = list(indexed.values()) + self.list self.skip_lines() + def __repr__(self): + return "{%s}" % ','.join("'%s': '%s'" % (k, v if len(v) > 1 else v[0]) for k, v in self.iteritems()) -class CustomByteLikeObject(object): - def __init__(self, data): - self.data = data - - def read(self, length=None): - if length: - return self.data[0:length] - else: - return self.data + @property + def __dict__(self): + d = {} + for key, value in self.iteritems(): + d[key] = value if len(value) > 1 else value[0] + return d - def readline(self, *args): - return self.data -def ParseForm(file_handle, environ, json=False): - """Returns an IndexedFieldStorage object from the POST data and environment. +class QueryArgsDict(dict): + def getfirst(self, key, default=None): + """Returns the first value for the requested key, or a fallback value.""" + try: + return self[key][0] + except KeyError: + return default - This small wrapper is necessary because cgi.FieldStorage assumes that the - provided file handles supports .readline() iteration. File handles as provided - by BaseHTTPServer do not support this, so we need to convert them to proper - stringIO objects first. - """ - #TODO see if we need to encode in utf8 or is ascii is fine based on the headers - # print(file_handle.read(int(environ['CONTENT_LENGTH'])).decode('ascii')) - # data = sys.stdin.read() - if json: - #We already decoded the JSON and turned into a urlquerystring - environ['CONTENT_TYPE'] = 'application/x-www-form-urlencoded' - files = CustomByteLikeObject(file_handle.encode()) - else: - files = CustomByteLikeObject(file_handle.read(int(environ['CONTENT_LENGTH']))) + def getlist(self, key): + """Returns a list with all values that were given for the requested key. - return IndexedFieldStorage(fp=files, environ=environ, keep_blank_values=1) + N.B. If the given key does not exist, an empty list is returned. + """ + try: + return self[key] + except KeyError: + return [] +def return_real_remote_addr(env): + """Returns the remote ip-address, + if there is a proxy involved it will take the last IP addres from the HTTP_X_FORWARDED_FOR list + """ + try: + return env['HTTP_X_FORWARDED_FOR'].split(',')[-1].strip() + except KeyError: + return env['REMOTE_ADDR'] diff --git a/uweb3/response.py b/uweb3/response.py index 63cf1d23..ca339820 100644 --- a/uweb3/response.py +++ b/uweb3/response.py @@ -7,7 +7,10 @@ except ImportError: import http.client as httplib +import json + from collections import defaultdict +from .libs.safestring import JSONsafestring class Response(object): """Defines a full HTTP response. @@ -34,10 +37,12 @@ def __init__(self, content='', content_type=CONTENT_TYPE, A dictionary with header names and their associated values. """ self.charset = kwds.get('charset', 'utf8') + self.content = None self.text = content self.httpcode = httpcode self.headers = headers or {} - if ';' not in content_type: + if (content_type.startswith('text/') or + content_type.startswith('application/json')) and ';' not in content_type: content_type = '{!s}; charset={!s}'.format(content_type, self.charset) self.content_type = content_type @@ -53,6 +58,11 @@ def content_type(self, content_type): content_type = '{!s}; {!s}'.format(content_type, current.split(';', 1)[-1]) self.headers['Content-Type'] = content_type + def clean_content_type(self): + if ';' not in self.headers['Content-Type']: + return self.headers['Content-Type'] + return self.headers['Content-Type'].split(';')[0] + # Get and set body text @property def text(self): @@ -60,7 +70,7 @@ def text(self): @text.setter def text(self, content): - self.content = str(content) + self.content = content # Retrieve a header list @property @@ -94,11 +104,9 @@ class Redirect(Response): REDIRECT_PAGE = ('Page moved' 'Page moved, please follow this link' '') - #TODO make sure we inject cookies set on the previous response by copying any Set-Cookie headers from them into these headers. def __init__(self, location, httpcode=307): super(Redirect, self).__init__( content=self.REDIRECT_PAGE % location, content_type='text/html', httpcode=httpcode, headers={'Location': location}) - diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index 5dac7757..2f11aca0 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -17,9 +17,10 @@ import os import re import urllib.parse as urlparse -from .ext_lib.libs.safestring import * +from .libs.safestring import * import hashlib import itertools +import ast, math class Error(Exception): """Superclass used for inheritance and external exception handling.""" @@ -49,6 +50,10 @@ class TemplateReadError(Error, IOError): """Template file could not be read or found.""" +class TemplateEvaluationError(Error): + """Template condition was not within allowed set of operators.""" + + class LazyTagValueRetrieval(object): """Provides a means for lazy tag value retrieval. @@ -99,6 +104,17 @@ def values(self): """Returns a list with the values of the LazyTagValueRetrieval dict.""" return list(self.itervalues()) +EVALWHITELIST = { + 'functions': {"abs": abs, "complex": complex, "min": min, "max": max, + "pow": pow, "round": round, "len": len, "type": type, + "isinstance": isinstance, + **{key: value for (key,value) in vars(math).items() if not key.startswith('__')}}, + 'operators': (ast.Module, ast.Expr, ast.Load, ast.Expression, ast.Add, ast.And, + ast.Sub, ast.UnaryOp, ast.Num, ast.BinOp, ast.Mult, ast.Gt, + ast.Div, ast.Pow, ast.BitOr, ast.BitAnd, ast.BitXor, ast.Lt, + ast.USub, ast.UAdd, ast.FloorDiv, ast.Mod, ast.LShift, + ast.RShift, ast.Invert, ast.Call, ast.Name, ast.Compare, + ast.Eq, ast.NotEq, ast.Not, ast.Or, ast.BoolOp, ast.Str)} class Parser(dict): """A template parser that loads and caches templates and parses them by name. @@ -139,15 +155,13 @@ def __init__(self, path='.', templates=(), noparse=False): super(Parser, self).__init__() self.template_dir = path self.noparse = noparse - - self.messages = None - self.templates = None - self.storage = None + self.tags = {} + self.requesttags = {} + self.astvisitor = AstVisitor(EVALWHITELIST) for template in templates: self.AddTemplate(template) - def __getitem__(self, template): """Retrieves a stored template by name. @@ -167,7 +181,7 @@ def __getitem__(self, template): """ if template not in self: self.AddTemplate(template) - return super(Parser, self).__getitem__(template) + return super().__getitem__(template) def AddTemplate(self, location, name=None): """Reads the given `template` filename and adds it to the cache. @@ -207,10 +221,10 @@ def Parse(self, template, **replacements): Returns: str: The template with relevant tags replaced by the replacement dict. """ - - replacements['messages'] = self.messages - replacements['storage'] = self.storage - replacements.update(self.templates) + if self.tags: + replacements.update(self.tags) + if self.requesttags: + replacements.update(self.requesttags) return self[template].Parse(**replacements) def ParseString(self, template, **replacements): @@ -227,6 +241,10 @@ def ParseString(self, template, **replacements): Returns: str: template with replaced tags. """ + if self.tags: + replacements.update(self.tags) + if self.requesttags: + replacements.update(self.requesttags) return Template(template, parser=self).Parse(**replacements) @staticmethod @@ -241,6 +259,53 @@ def RegisterFunction(name, function): """ TAG_FUNCTIONS[name] = function + def RegisterTag(self, tag, value, persistent=False): + """Registers a `value`, allowing use in templates by `tag`. + + Arguments: + @ tag: str + The name of the tag + @ value: str, or function + Value or function to be executed when replacing this tag + @ persistent: bool + will this tag be present for multiple requests? + """ + if persistent: + storage = self.tags + else: + storage = self.requesttags + if ':' not in tag: + storage[tag] = value + return + tag = TemplateTag.FromString('[%s]' % tag) + # if we are dealing with a tag consisting of multiple path parts, lets reconstruct the path + obj = storage + prevnode = tag.name + d = storage + for node in tag.indices: + try: + node = int(node) + subtype = SparseList() + except ValueError: + subtype = {} + + # add the new sublist to the path if not existant + if prevnode not in obj: + obj[prevnode] = subtype + + obj = obj[prevnode] + prevnode = node + obj[node] = value + + @classmethod + def JITTag(cls, function): + return JITTag(function) + + def ClearRequestTags(self): + """Resets the non persistent tags to None, is to be called after each + completed request""" + self.requesttags = {} + TemplateReadError = TemplateReadError @@ -267,7 +332,7 @@ def __init__(self, raw_template, parser=None): An optional parser instance that is necessary to enable support for adding files to the current template. This is used by {{ inline }}. """ - super(Template, self).__init__() + super().__init__() self.parser = parser self.scopes = [self] self.AddString(raw_template) @@ -329,7 +394,7 @@ def AddString(self, raw_template): raise TemplateSyntaxError('Template left %d open scopes.' % scope_diff) def Parse(self, returnRawTemplate=False, **kwds): - """Returns the parsed template as SafeString. + """Returns the parsed template as HTMLsafestring. The template is parsed by parsing each of its members and combining that. """ @@ -341,9 +406,9 @@ def Parse(self, returnRawTemplate=False, **kwds): return raw if self.parser and self.parser.noparse: - #Hash the page so that we can compare on the frontend if the html has changed + # Hash the page so that we can compare on the frontend if the html has changed htmlsafe.page_hash = hashlib.md5(HTMLsafestring(self).encode()).hexdigest() - #Hashes the page and the content so we can know if we need to refresh the page on the frontend + # Hashes the page and the content so we can know if we need to refresh the page on the frontend htmlsafe.tags = {} for tag in self: if isinstance(tag, TemplateConditional): @@ -391,9 +456,6 @@ def _ExtendText(self, node): # Template syntax constructs # - def _TemplateConstructXsrf(self, value): - self.AddString(''.format(value)) - def _TemplateConstructInline(self, name): """Processing for {{ inline }} template syntax.""" self.AddFile(name) @@ -408,15 +470,18 @@ def _TemplateConstructEndfor(self): def _TemplateConstructIf(self, *nodes): """Processing for {{ if }} template syntax.""" - self._StartScope(TemplateConditional(' '.join(nodes))) + self._StartScope(TemplateConditional(' '.join(nodes), + self.parser.astvisitor if self.parser else AstVisitor(EVALWHITELIST))) def _TemplateConstructIfpresent(self, *nodes): """Processing for {{ ifpresent }} template syntax.""" - self._StartScope(TemplateConditionalPresence(' '.join(nodes))) + self._StartScope(TemplateConditionalPresence(' '.join(nodes), + self.parser.astvisitor if self.parser else AstVisitor(EVALWHITELIST))) def _TemplateConstructIfnotpresent(self, *nodes): """Processing for {{ ifnotpresent }} template syntax.""" - self._StartScope(TemplateConditionalPresence(' '.join(nodes), checking_presence=True)) + self._StartScope(TemplateConditionalNotPresence(' '.join(nodes), + self.parser.astvisitor if self.parser else AstVisitor(EVALWHITELIST))) def _TemplateConstructElif(self, *nodes): """Processing for {{ elif }} template syntax.""" @@ -482,7 +547,8 @@ def __init__(self, template_path, parser=None): try: self._file_name = os.path.abspath(template_path) self._file_mtime = os.path.getmtime(self._file_name) - raw_template = open(self._file_name).read() + with open(self._file_name) as templatefile: + raw_template = templatefile.read() super(FileTemplate, self).__init__(raw_template, parser=parser) except (IOError, OSError): raise TemplateReadError('Cannot open: %r' % template_path) @@ -493,13 +559,12 @@ def Parse(self, **kwds): The template is parsed by parsing each of its members and combining that. """ self.ReloadIfModified() - result = super(FileTemplate, self).Parse(**kwds) + result = super().Parse(**kwds) if self.parser and self.parser.noparse: - return {'template': self._file_name.rsplit('/')[-1], + return {'template': self._templatepath[len(self.parser.template_dir):], 'replacements': result.tags, 'content_hash':result.content_hash, - 'page_hash': result.page_hash - } + 'page_hash': result.page_hash} return result def ReloadIfModified(self): @@ -515,7 +580,8 @@ def ReloadIfModified(self): try: mtime = os.path.getmtime(self._file_name) if mtime > self._file_mtime: - template = open(self._file_name).read() + with open(self._file_name) as templatefile: + template = templatefile.read() del self[:] self.scopes = [self] self.AddString(template) @@ -528,11 +594,11 @@ def ReloadIfModified(self): class TemplateConditional(object): """A template construct to control flow based on the value of a tag.""" - def __init__(self, expr, checking_presence=True): - self.checking_presence = checking_presence + def __init__(self, expr, astvisitor): self.branches = [] self.default = None self.NewBranch(expr) + self.astvisitor = astvisitor def __repr__(self): repr_branches = [] @@ -584,9 +650,8 @@ def Else(self): raise TemplateSyntaxError('Only one {{ else }} clause is allowed.') self.default = [] - @staticmethod - def Expression(expr, **kwds): - """Returns the eval()'ed result of a tag expression.""" + def Expression(self, expr, **kwds): + """Returns the evaluated result of a tag expression.""" nodes = [] local_vars = LazyTagValueRetrieval(kwds) for num, node in enumerate(expr): @@ -597,8 +662,7 @@ def Expression(expr, **kwds): else: nodes.append(node) try: - #XXX(Elmer): This uses eval, it's so much easier than lexing and parsing - return eval(''.join(nodes), None, local_vars) + return LimitedEval(''.join(nodes), self.astvisitor, local_vars) except NameError as error: raise TemplateNameError(str(error).capitalize() + '. Try it as tagname?') @@ -621,8 +685,6 @@ def Parse(self, **kwds): `else` branch exists '' is returned. """ for expr, branch in self.branches: - if type(self) == TemplateConditionalPresence: - kwds['checking_presence'] = True if self.Expression(expr, **kwds): return ''.join(part.Parse(**kwds) for part in branch) if self.default: @@ -630,7 +692,6 @@ def Parse(self, **kwds): return '' - class TemplateConditionalPresence(TemplateConditional): """A template construct to safely check for the presence of tags.""" @@ -640,17 +701,28 @@ def Expression(tags, **kwds): try: for tag in tags: tag.GetValue(kwds) - if kwds.get('checking_presence'): - return True - return False - except (TemplateKeyError, TemplateNameError): - if kwds.get('checking_presence'): - return False return True + except (TemplateKeyError, TemplateNameError): + return False def NewBranch(self, tags): """Begins a new branch based on the given tags.""" - self.branches.append((map(TemplateTag.FromString, tags.split()), [])) + self.branches.append((list(map(TemplateTag.FromString, tags.split())), [])) + + +class TemplateConditionalNotPresence(TemplateConditionalPresence): + """A template construct to safely check for the presence of tags.""" + + @staticmethod + def Expression(tags, **kwds): + """Checks the presence of all tags named on the branch.""" + try: + for tag in tags: + tag.GetValue(kwds) + return False + except (TemplateKeyError, TemplateNameError): + return True + class TemplateLoop(list): """Template loops are used to repeat a portion of template multiple times. @@ -729,6 +801,7 @@ class TemplateTag(object): re.VERBOSE) FUNC_FINDER = re.compile('\|([\w-]+(?:\([^()]*?\))?)') FUNC_CLOSURE = re.compile('(\w+)\((.*)\)') + ALLOWPRIVATE = False # will we allow access to private members for object lookup def __init__(self, name, indices=(), functions=()): """Initializes a TemplateTag instant. @@ -742,10 +815,9 @@ def __init__(self, name, indices=(), functions=()): Names of template functions that should be applied to the value. """ self.name = name - self.indices = indices + self.indices = indices if self.ALLOWPRIVATE else list(index for index in indices if not index.startswith('_') or not index.endswith('_')) self.functions = functions - def __repr__(self): return '%s(%r)' % (type(self).__name__, str(self)) @@ -795,6 +867,8 @@ def GetValue(self, replacements): value = replacements[self.name] for index in self.indices: value = self._GetIndex(value, index) + if isinstance(value, JITTag): + return value() return value except KeyError: raise TemplateNameError('No replacement with name %r' % self.name) @@ -807,7 +881,9 @@ def ApplyFunction(cls, func, value): return TAG_FUNCTIONS[func](value) func, args = closure.groups() #XXX(Elmer): This uses eval, it's so much easier than lexing and parsing - args = eval(args + ',') if args.strip() else () + # the regex leading up to this point make sure no function calls end up in + # here, nor variables, Math might show up though + args = eval(args + ',', {'__builtins__': {}}, {}) if args.strip() else () return TAG_FUNCTIONS[func](*args)(value) except SyntaxError: raise TemplateSyntaxError('Invalid argument syntax: %r' % args) @@ -817,6 +893,9 @@ def ApplyFunction(cls, func, value): except KeyError as err_obj: raise TemplateNameError( 'Unknown template tag function %r' % err_obj.args[0]) + except NameError as err_obj: + raise TemplateSyntaxError( + 'Access to scope outside of parser variables is not allowed: %r' % err_obj.args[0]) def Parse(self, **kwds): """Returns the parsed string of the tag, using given replacements. @@ -842,14 +921,13 @@ def Parse(self, **kwds): except (TemplateKeyError, TemplateNameError): # On any failure to get the given index, return the unmodified tag. return str(self) - # Process functions, or apply default if value is not HTMLsafestring + # Process functions, or apply default if value is not Basesafestring if self.functions: for func in self.functions: value = self.ApplyFunction(func, value) - else: - if not isinstance(value, Basesafestring): - value = TAG_FUNCTIONS['default'](value) - return str(value) + if not isinstance(value, Basesafestring): + value = TAG_FUNCTIONS['default'](value) + return value def Iterator(self, **kwds): """Parses the tag for iteration purposes. @@ -866,7 +944,6 @@ def Iterator(self, **kwds): value = TAG_FUNCTIONS[func](value) return iter(value) - @staticmethod def _GetIndex(haystack, needle): """Returns the `needle` from the `haystack` by index, key or attribute name. @@ -882,7 +959,7 @@ def _GetIndex(haystack, needle): Returns: obj: the object existing on `needle` in `haystack`. - """ + """ try: if needle.isdigit(): try: @@ -905,7 +982,7 @@ def _GetIndex(haystack, needle): class TemplateText(str): """A raw piece of template text, upon which no replacements will be done.""" def __new__(cls, string): - return super(TemplateText, cls).__new__(cls, string) + return super().__new__(cls, string) def __repr__(self): """Returns the object representation of the TemplateText.""" @@ -916,11 +993,72 @@ def Parse(self, **_kwds): return str(self) +class JITTag(object): + """This is a template Tag which is only evaulated on replacement. + It is usefull for situations where not all all of this functions input vars + are available just yet. + """ + + def __init__(self, function): + """Stores the function for later use""" + self.wrapped = function + self.result = None # cache for results + self.called = False # keep score of result cache usage, None and False might be correct results in the cache + + def __call__(self): + """Returns the output of the earlier wrapped function""" + if not self.called: + self.result = self.wrapped() + self.called = True + return self.result + + +class SparseList(list): + """A spare list implementation to allow us to set the nth item on a list""" + def __setitem__(self, index, value): + missing = index - len(self) + 1 + if missing > 0: + self.extend([None] * missing) + super().__setitem__(index, value) + + def __getitem__(self, index): + """Return the value at the index, and None of that index was not available + instead of raisig IndexError + """ + try: + return super().__getitem__(index) + except IndexError: + return None + + +class AstVisitor(ast.NodeVisitor): + def __init__(self, whitelists): + self.whitelists = whitelists + + def visit(self, node): + if not isinstance(node, self.whitelists['operators']): + raise TemplateEvaluationError('`%s` is not an allowed operation' % node) + return super().visit(node) + + def visit_Call(self, call): + """Filter calls""" + if call.func.id not in self.whitelists['functions']: + raise TemplateEvaluationError('`%s` is not an allowed function call' % call.func.id) + +def LimitedEval(expr, astvisitor, evallocals = {}): + tree = ast.parse(expr, mode='eval') + astvisitor.visit(tree) + return eval(compile(tree, "", "eval"), + astvisitor.whitelists['functions'], + evallocals) + + TAG_FUNCTIONS = { 'default': lambda d: HTMLsafestring('') + d, 'html': lambda d: HTMLsafestring('') + d, - 'raw': lambda x: x, - 'url': lambda d: URLqueryargumentsafestring(d, unsafe=True), + 'raw': lambda d: Unsafestring(d), + 'url': lambda d: HTMLsafestring(URLqueryargumentsafestring(d, unsafe=True)), + 'type': type, 'items': lambda d: list(d.items()), 'values': lambda d: list(d.values()), 'sorted': sorted, diff --git a/uweb3/test_model.py b/uweb3/test_model.py old mode 100644 new mode 100755 diff --git a/uweb3/test_model_alchemy.py b/uweb3/test_model_alchemy.py old mode 100644 new mode 100755 diff --git a/uweb3/test_request.py b/uweb3/test_request.py old mode 100644 new mode 100755 index e4d5c7c5..2bcea9ff --- a/uweb3/test_request.py +++ b/uweb3/test_request.py @@ -1,4 +1,5 @@ -#!/usr/bin/python +#!/usr/bin/python3 +# -*- coding: utf-8 -*- """Tests for the request module.""" # Method could be a function @@ -17,7 +18,7 @@ import urllib # Unittest target -import request +from uweb3 import request class IndexedFieldStorageTest(unittest.TestCase): @@ -36,7 +37,6 @@ def testEmptyStorage(self): def testBasicStorage(self): """A basic IndexedFieldStorage has the proper key + value pair""" ifs = self.CreateFieldStorage('key=value') - self.assertTrue(ifs) self.assertEqual(ifs.getfirst('key'), 'value') self.assertEqual(ifs.getlist('key'), ['value']) diff --git a/uweb3/test_templateparser.py b/uweb3/test_templateparser.py old mode 100644 new mode 100755 index b3af77f9..acb6f296 --- a/uweb3/test_templateparser.py +++ b/uweb3/test_templateparser.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 """Tests for the templateparser module.""" # Too many public methods @@ -145,6 +145,11 @@ def testUnreplacedTag(self): template = 'Template with an [undefined] tag.' self.assertEqual(self.tmpl(template).Parse(), template) + def testUnreplacedTag(self): + """[BasicTag] Access to private members is not allowed""" + template = 'Template with an [private.__class__] tag.' + self.assertEqual(self.tmpl(template).Parse(), template) + def testBracketsInsideTag(self): """[BasicTag] Innermost bracket pair are the tag's delimiters""" template = 'Template tags may not contain [[spam][eggs]].' @@ -196,8 +201,11 @@ class Mapping(dict): self.assertEqual(self.tmpl(template).Parse(tag=mapp), lookup_dict) def testTemplateIndexingCharacters(self): - """[IndexedTag] Tags indexes may be made of word chars and dashes only""" - good_chars = "aAzZ0123-_" + """[IndexedTag] Tags indexes may be made of word chars and dashes only, + they should however not start and end with _ to avoid access to + private vars. + _ is allowed elsewhere in the string.""" + good_chars = "aAzZ0123-" bad_chars = """ :~!@#$%^&*()+={}\|;':",./<>? """ for index in good_chars: tag = {index: 'SUCCESS'} @@ -208,6 +216,29 @@ def testTemplateIndexingCharacters(self): template = '[tag:%s]' % index self.assertEqual(self.tmpl(template).Parse(tag=tag), template) + def testTemplateUnderscoreCharacters(self): + """[IndexedTag] Tags indexes may be made of word chars and dashes only, + they should however not start and end with _ to avoid access to + private vars. + _ is allowed elsewhere in the string.""" + # see if objects with underscores are reachable + tag = {'test_test': 'SUCCESS'} + template = '[tag:%s]' % 'test_test' + self.assertEqual(self.tmpl(template).Parse(tag=tag), 'SUCCESS') + + tag = {'_test': 'SUCCESS'} + template = '[tag:%s]' % '_test' + self.assertEqual(self.tmpl(template).Parse(tag=tag), 'SUCCESS') + + tag = {'test_': 'SUCCESS'} + template = '[tag:%s]' % 'test_' + self.assertEqual(self.tmpl(template).Parse(tag=tag), 'SUCCESS') + + # check if private vars are impossible to reach. + tag = {'_test_': 'SUCCESS'} + template = '[tag:%s|raw]' % '_test_' + self.assertEqual(self.tmpl(template).Parse(tag=tag), repr(tag)) + def testTemplateMissingIndexes(self): """[IndexedTag] Tags with bad indexes will be returned verbatim""" class Object(object): @@ -234,6 +265,18 @@ def setUp(self): self.parse = self.parser.ParseString def testBasicFunction(self): + """[TagFunctions] and html safe output""" + template = 'This function does [none].' + result = self.parse(template, none='"nothing"') + self.assertEqual(result, 'This function does "nothing".') + + def testBasicFunctionNumeric(self): + """[TagFunctions] and html safe output for non string outputs""" + template = '[tag]' + result = self.parse(template, tag=1) + self.assertEqual(result, '1') + + def testBasicFunctionRaw(self): """[TagFunctions] Raw function does not affect output""" template = 'This function does [none|raw].' result = self.parse(template, none='"nothing"') @@ -326,14 +369,14 @@ def testTagFunctionUrl(self): def testTagFunctionItems(self): """[TagFunctions] The tag function 'items' is present and works""" - template = '[tag|items]' + template = '[tag|items|raw]' tag = {'ham': 'eggs'} result = "[('ham', 'eggs')]" self.assertEqual(result, self.parse(template, tag=tag)) def testTagFunctionValues(self): """[TagFunctions] The tag function 'values' is present and works""" - template = '[tag|values]' + template = '[tag|values|raw]' self.assertEqual(self.parse(template, tag={'ham': 'eggs'}), "['eggs']") def testTagFunctionSorted(self): @@ -383,6 +426,27 @@ def testSimpleClosureArgument(self): result = self.parse(template, tag=self.tag) self.assertEqual(result, self.tag[:20]) + def testMathClosureArgument(self): + """[TagClosures] Math tag-closure functions operate on their argument""" + template = '[tag|limit(5*4)]' + result = self.parse(template, tag=self.tag) + self.assertEqual(result, self.tag[:20]) + + def testFunctionClosureArgument(self): + """[TagClosures] tags that use function calls in their function input should + never be parsed""" + template = '[tag|limit(abs(-20))]' + result = self.parse(template, tag=self.tag) + self.assertEqual(result, template) + + def testVariableClosureArgument(self): + """[TagClosures] tags that try to use vars in their function arguments + should never have access to the python scope.""" + test = 20 + template = '[tag|limit(test)]' + self.assertRaises(templateparser.TemplateSyntaxError, + self.parse, template, tag=self.tag) + def testComplexClosureWithoutArguments(self): """[TagClosures] Complex tag closure-functions without arguments succeed""" template = '[tag|strlimit()]' @@ -401,7 +465,7 @@ def testComplexClosureArguments(self): def testCharactersInClosureArguments(self): """[TagClosures] Arguments strings may contain specialchars""" - template = '[tag|strlimit(20, "`-=./<>?`!@#$%^&*_+[]\{}|;\':")]' + template = '[tag|strlimit(20, "`-=./<>?`!@#$%^&*_+[]\{}|;\':")|raw]' result = self.parser.ParseString(template, tag=self.tag) self.assertTrue(result.endswith('`-=./<>?`!@#$%^&*_+[]\{}|;\':')) @@ -521,13 +585,28 @@ def testCompareTag(self): self.assertFalse(self.parse(template, variable=12)) self.assertTrue(self.parse(template, variable=5)) + def testCompareMath(self): + """{{ if }} Basic math""" + template = '{{ if 5*5 == 25 }} foo {{ endif }}' + self.assertEqual(self.parse(template, variable=5).strip(), 'foo') + def testTagIsInstance(self): - """{{ if }} Basic tag value comparison""" + """{{ if }} Tag value after python function comparison""" template = '{{ if isinstance([variable], int) }} ack {{ endif }}' self.assertFalse(self.parse(template, variable=[1])) self.assertFalse(self.parse(template, variable='number')) self.assertEqual(self.parse(template, variable=5), ' ack ') + def testComparePythonFunction(self): + """{{ if }} Tag value after python len comparison""" + template = '{{ if len([variable]) == 5 }} foo {{ endif }}' + self.assertEqual(self.parse(template, variable=[1,2,3,4,5]).strip(), 'foo') + + def testCompareNotallowdPythonFunction(self): + """{{ if }} Tag value after python len comparison""" + template = '{{ if open([variable]) == 5 }} foo {{ endif }}' + self.assertRaises(templateparser.TemplateEvaluationError, self.parse, template) + def testDefaultElse(self): """{{ if }} Else block will be parsed when `if` fails""" template = '{{ if [var] }}foo{{ else }}bar{{ endif }}' @@ -689,7 +768,9 @@ def testLoopAbsentIndex(self): class TemplateTagPresenceCheck(unittest.TestCase): """Test cases for the `ifpresent` TemplateParser construct.""" def setUp(self): - self.parse = templateparser.Parser().ParseString + self.parser = templateparser.Parser() + self.parse = self.parser.ParseString + self.templatefilename = 'ifpresent.utp' def testBasicTagPresence(self): """{{ ifpresent }} runs the code block if the tag is present""" @@ -701,6 +782,20 @@ def testBasicTagAbsence(self): template = '{{ ifpresent [tag] }} hello {{ endif }}' self.assertFalse(self.parse(template)) + def testBasicTagNotPresence(self): + """{{ ifnotpresent }} runs the code block if the tag is present""" + template = '{{ ifnotpresent [tag] }} hello {{ endif }}' + self.assertEqual(self.parse(template, othertag='spam'), ' hello ') + + def testNestedNotPresence(self): + """{{ ifnotpresent }} runs the code block if the tag is present""" + template = """{{ ifnotpresent [tag] }} + {{ ifnotpresent [nestedtag] }} + hello + {{ endif }} + {{ endif }}""" + self.assertEqual(self.parse(template, othertag='spam').strip(), 'hello') + def testTagPresenceElse(self): """{{ ifpresent }} has a functioning `else` clause""" template = '{{ ifpresent [tag] }} yes {{ else }} no {{ endif }}' @@ -734,6 +829,25 @@ def testBadSyntax(self): template = '{{ ifpresent var }} {{ endif }}' self.assertRaises(templateparser.TemplateSyntaxError, self.parse, template) + def testMultiTagPresenceFile(self): + """{{ ifpresent }} checks if multiple runs on a file template containing an + Ifpresent block work""" + + template = '{{ ifpresent [one] }} [one] {{ endif }}Blank' + with open(self.templatefilename, 'w') as templatefile: + templatefile.write(template) + self.assertEqual(self.parser.Parse(self.templatefilename), 'Blank') + #self.assertEqual(self.parser.Parse(self.templatefilename), 'Blank') + #self.assertEqual(self.parser.Parse(self.templatefilename, one=1), ' 1 Blank') + + def tearDown(self): + for tmpfile in (self.templatefilename, ): + if os.path.exists(tmpfile): + if os.path.isdir(tmpfile): + os.rmdir(tmpfile) + else: + os.unlink(tmpfile) + class TemplateStringRepresentations(unittest.TestCase): """Test cases for string representation of various TemplateParser parts.""" From a9c1670a932f6735f1eb9295e3c643d5c1360a89 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 27 Oct 2020 12:08:50 +0100 Subject: [PATCH 041/118] make mail module use safestring module, use machine name when no local hostname was given --- uweb3/libs/mail.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/uweb3/libs/mail.py b/uweb3/libs/mail.py index 818fb729..22a1c8cd 100644 --- a/uweb3/libs/mail.py +++ b/uweb3/libs/mail.py @@ -11,6 +11,7 @@ from email.mime.base import MIMEBase from email.mime.text import MIMEText from email.mime.multipart import MIMEMultipart +from uweb3.libs.safestring import EmailAddresssafestring, EmailHeadersafestring class MailError(Exception): @@ -20,7 +21,7 @@ class MailError(Exception): class MailSender(object): """Easy context-interface for sending mail.""" def __init__(self, host='localhost', port=25, - local_hostname='localhost', timeout=5): + local_hostname=None, timeout=5): """Sets up the connection to the SMTP server. Arguments: @@ -28,14 +29,15 @@ def __init__(self, host='localhost', port=25, The SMTP hostname to connect to. % port: int ~~ 25 Port for the SMTP server. - % local_hostname: str ~~ 'underdark.nl' + % local_hostname: str ~~ from local hostname The hostname for which we want to send messages. % timeout: int ~~ 5 Timeout in seconds. """ self.server = None self.options = {'host': host, 'port': port, - 'local_hostname': local_hostname, 'timeout': timeout} + 'local_hostname': local_hostname or os.uname()[1], + 'timeout': timeout} def __enter__(self): """Returns a SendMailContext for sending emails.""" @@ -75,9 +77,9 @@ def Text(self, recipients, subject, content, Character set to encode mail to. """ message = MIMEMultipart() - message['From'] = sender or self.Noreply() + message['From'] = EmailAddresssafestring('') + (sender or self.Noreply()) message['To'] = self.ParseRecipients(recipients) - message['Subject'] = ' '.join(subject.strip().split()) + message['Subject'] = EmailHeadersafestring('') + ' '.join(subject.strip().split()) message.attach(MIMEText(content.encode(charset), 'plain', charset)) if reply_to: message['Reply-to'] = self.ParseRecipients(reply_to) @@ -92,9 +94,9 @@ def Attachments(self, recipients, subject, content, Content in case of 2-tuple can be `str` or any file-like object. """ message = MIMEMultipart() - message['From'] = sender or self.Noreply() + message['From'] = EmailAddresssafestring('') + (sender or self.Noreply()) message['To'] = self.ParseRecipients(recipients) - message['Subject'] = ' '.join(subject.strip().split()) + message['Subject'] = EmailHeadersafestring('') + ' '.join(subject.strip().split()) if reply_to: message['Reply-to'] = self.ParseRecipients(reply_to) message.attach(MIMEText(content.encode(charset), 'plain', charset)) @@ -128,21 +130,17 @@ def ParseAttachment(attachment): @staticmethod def ParseRecipients(recipients): - """Ensures multiple recipients are returned as a string without newlines.""" + """Ensures multiple recipients are returned as a safestring without + newlines.""" if isinstance(recipients, str): - return StripNewlines(recipients) - return ', '.join(map(StripNewlines, recipients)) + return EmailAddresssafestring('') + recipients + return EmailAddresssafestring('') + ', '.join(recipients) def Noreply(self): """Returns the no-reply email address for the configured local hostname.""" return 'no-reply ' % self.server.local_hostname -def StripNewlines(text): - """Replaces newlines and tabs with a single space.""" - return ' '.join(text.strip().split()) - - def Wrap(content, cols=76): """Wraps multipart mime content into 76 column lines for niceness.""" lines = [] From fadc2853f82f73840492cca3db952aae1b0a1884 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 27 Oct 2020 12:09:47 +0100 Subject: [PATCH 042/118] add email header safestring class --- uweb3/libs/safestring/__init__.py | 16 +++++++++++++++- uweb3/libs/safestring/test.py | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/uweb3/libs/safestring/__init__.py b/uweb3/libs/safestring/__init__.py index 70b5b403..1e6ca39e 100644 --- a/uweb3/libs/safestring/__init__.py +++ b/uweb3/libs/safestring/__init__.py @@ -279,7 +279,7 @@ def unescape(self, data): class EmailAddresssafestring(Basesafestring): - """This class signals that the content is safe Email address + """This class signals that the content is a safe Email address Its usefull when sending out emails or constructing email headers Email Header injection is subverted. @@ -298,3 +298,17 @@ def escape(self, data): def unescape(self, data): """Can't unremove non address elements so we'll just return the string""" return data + + +class EmailHeadersafestring(Basesafestring): + """This class signals that the content is a safe Email header + + Its usefull when sending out emails or constructing email headers.""" + + def escape(self, data): + """Drops everything that does not fit in a email header""" + return data.replace("\n", "").replace("\r", "") + + def unescape(self, data): + """Can't unremove non header elements so we'll just return the string""" + return data diff --git a/uweb3/libs/safestring/test.py b/uweb3/libs/safestring/test.py index 517b8a74..1fba185e 100755 --- a/uweb3/libs/safestring/test.py +++ b/uweb3/libs/safestring/test.py @@ -7,7 +7,7 @@ import unittest #custom modules -from uweb3.ext_lib.libs.safestring import URLsafestring, SQLSAFE, HTMLsafestring, URLqueryargumentsafestring, JSONsafestring, EmailAddresssafestring, Basesafestring +from uweb3.libs.safestring import URLsafestring, SQLSAFE, HTMLsafestring, URLqueryargumentsafestring, JSONsafestring, EmailAddresssafestring, Basesafestring class BasesafestringMethods(unittest.TestCase): def test_creation_str(self): From 5a323b9e6eadf5eb9259df5e7e2860bf0b52f8ba Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 27 Oct 2020 12:10:12 +0100 Subject: [PATCH 043/118] remove old uweb start stop script --- uweb3/scripts/__init__.py | 0 uweb3/scripts/tables.py | 45 ----- uweb3/scripts/uweb | 386 -------------------------------------- 3 files changed, 431 deletions(-) delete mode 100644 uweb3/scripts/__init__.py delete mode 100644 uweb3/scripts/tables.py delete mode 100755 uweb3/scripts/uweb diff --git a/uweb3/scripts/__init__.py b/uweb3/scripts/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/uweb3/scripts/tables.py b/uweb3/scripts/tables.py deleted file mode 100644 index f9b0a2d1..00000000 --- a/uweb3/scripts/tables.py +++ /dev/null @@ -1,45 +0,0 @@ -# Originally from: http://code.activestate.com/recipes/577202/#c4 -# Written by Vasilij Pupkin (2012) -# Minor changes by Elmer de Looff (2012) -# Licensed under the MIT License (http://opensource.org/licenses/MIT - - -class ALIGN(object): - LEFT, RIGHT = '-', '' - -class Column(list): - def __init__(self, name, data, align=ALIGN.LEFT): - list.__init__(self, data) - self.name = name - self.width = max(len(x) for x in self + [name]) - self.format = ' %%%s%ds ' % (align, self.width) - -class Table(object): - def __init__(self, *columns): - self.columns = columns - self.length = max(len(x) for x in columns) - - def get_row(self, i=None): - for x in self.columns: - if i is None: - yield x.format % x.name - else: - yield x.format % x[i] - - def get_line(self): - for x in self.columns: - yield '-' * (x.width + 2) - - def join_n_wrap(self, char, elements): - return ' ' + char + char.join(elements) + char - - def get_rows(self): - yield self.join_n_wrap('+', self.get_line()) - yield self.join_n_wrap('|', self.get_row(None)) - yield self.join_n_wrap('+', self.get_line()) - for i in range(0, self.length): - yield self.join_n_wrap('|', self.get_row(i)) - yield self.join_n_wrap('+', self.get_line()) - - def __str__(self): - return '\n'.join(self.get_rows()) diff --git a/uweb3/scripts/uweb b/uweb3/scripts/uweb deleted file mode 100755 index 464da648..00000000 --- a/uweb3/scripts/uweb +++ /dev/null @@ -1,386 +0,0 @@ -#!/usr/bin/python -"""uWeb development server management script""" - -import os -import shutil -import simplejson -import sys -import logging -import subprocess -from optparse import OptionParser - -# Application specific modules -import uweb -from uweb.scripts import tables - - -class Error(Exception): - """Base class for application errors.""" - - -class UwebSites(object): - """Abstraction for the uWeb site managing JSON file.""" - SITES_BASE = {'uweb_info': {'router': 'uweb.uweb_info.router.uweb_info', - 'workdir': '/'}, - 'logviewer': {'router': 'uweb.logviewer.router.logging', - 'workdir': '/'}} - - def __init__(self): - self.sites_file = os.path.expanduser('~/.uweb/sites.json') - self.sites = self._LoadSites() - - def _InstallBaseSites(self): - """Create sites file with default data, and directory where necessary.""" - dirname = os.path.dirname(self.sites_file) - if not os.path.isdir(dirname): - print '.. no uweb data directory; creating %r' % dirname - os.mkdir(os.path.dirname(self.sites_file)) - with file(self.sites_file, 'w') as sites: - print '.. creating %r with default sites' % self.sites_file - sites.write(simplejson.dumps(self.SITES_BASE)) - print '' - - def _LoadSites(self): - """Load the sites file and return parsed JSON.""" - if not os.path.exists(self.sites_file): - self._InstallBaseSites() - with file(self.sites_file) as sites: - try: - return simplejson.loads(sites.read()) - except simplejson.JSONDecodeError: - raise Error('Could not read %r: Illegal JSON syntax' % self.sites_file) - - def _WriteSites(self): - """Write a new sites file after changes were made.""" - with file(self.sites_file, 'w') as sites: - sites.write(simplejson.dumps(self.sites)) - - def __contains__(self, key): - return key in self.sites - - def __iter__(self): - return iter(sorted(self.sites.items())) - - def __nonzero__(self): - return bool(self.sites) - - def __getitem__(self, name): - return self.sites[name] - - def __setitem__(self, name, router): - self.sites[name] = router - self._WriteSites() - - def __delitem__(self, name): - if name not in self.sites: - raise ValueError('There is no site with name %r' % name) - del self.sites[name] - self._WriteSites() - - -class BaseOperation(object): - """A simple class which parses command line values and call's it self.""" - def ParseCall(self): - """Base method to parse arguments and options.""" - raise NotImplementedError - - @staticmethod - def Banner(message): - line = '-' * 62 - return '+%s+\n| %-60s |\n+%s+' % (line, message[:60], line) - - def Run(self): - """Default method to parse arguments/options and activate class""" - opts, args = self.ParseCall() - self(*args[1:], **opts) - - def __call__(self, *args, **kwds): - """Base method to activate class""" - raise NotImplementedError - - -# ############################################################################## -# Initialization of and Apache configuration for projects -# -class Init(BaseOperation): - """Initialize uweb generator which create new uweb instance""" - # Base directory where the uWeb library lives - ROUTER_PATH = 'router' - ROUTER_NAME = 'router.py' - APACHE_CONFIG_NAME = 'apache.conf' - - def ParseCall(self): - parser = OptionParser(add_help_option=False) - parser.add_option('-f', '--force', action='store_true', - default=False, dest='force') - parser.add_option('-h', '--host', action='store', dest='host') - parser.add_option('-p', '--path', action='store', - default=os.getcwd(), dest='path') - parser.add_option('-s', '--silent', action='store_true', - default=False, dest='silent') - - opts, args = parser.parse_args() - return vars(opts), args - - def __call__(self, name=None, force=False, path=None, silent=False, - host='uweb.local'): - if name is None: - raise Error('Initialization requires a project name.') - project_path = os.path.abspath(name) - source_path = os.path.dirname('%s/base_project/' % uweb.__path__[0]) - apache_path = os.path.join(project_path, self.APACHE_CONFIG_NAME) - - print self.Banner('initializing new uWeb project %r' % name) - if os.path.exists(project_path): - if force: - print '* Removing existing project directory' - shutil.rmtree(project_path) - else: - raise Error('Target already exists, use -f (force) to overwrite.') - print '* copying uWeb base project directory' - shutil.copytree(source_path, project_path) - print '* setting up router' - # Rename default name 'router' to that of the project. - shutil.move( - os.path.join(project_path, self.ROUTER_PATH, self.ROUTER_NAME), - os.path.join(project_path, self.ROUTER_PATH, '%s.py' % name)) - - print '* setting up apache config' - GenerateApacheConfig.WriteApacheConfig( - name, host, apache_path, project_path) - - # Make sure we add the project to the sites list - sites = UwebSites() - sites[name] = {'router': '%s.router.%s' % (name, name), - 'workdir': os.getcwd()} - print self.Banner('initialization complete - have fun with uWeb') - - -class GenerateApacheConfig(BaseOperation): - """Generate apache config file for uweb project""" - def ParseCall(self): - parser = OptionParser(add_help_option=True) - parser.add_option('-n', - '--name', - action='store', - default='uweb_project', - dest='name') - - parser.add_option('-p', - '--path', - action='store', - default=os.getcwd(), - dest='path') - opts, args = parser.parse_args() - return vars(opts), args - - def __call__(self, name, host, path): - """Returns apache config string based on arguments""" - return ('\n' - ' documentroot %(path)s\n' - ' servername %(host)s\n' - '\n\n' - '\n' - ' SetHandler mod_python\n' - ' PythonHandler %(name)s\n' - ' PythonAutoReload on\n' - ' PythonDebug on\n' - '') % {'path': path, 'name': name, 'host': host} - - @staticmethod - def WriteApacheConfig(name, host, apache_config_path, project_path): - """write apache config file""" - with open(apache_config_path, 'w') as apache_file: - string = GenerateApacheConfig()(name, host, project_path) - apache_file.write(string) - - -# ############################################################################## -# Commands to manage configured uWeb sites. -# -class ListSites(BaseOperation): - """Print available uweb sites.""" - def ParseCall(self): - return {}, () - - def __call__(self, *args): - sites = UwebSites() - if not sites: - raise Error('No configured uWeb sites.') - print 'Overview of active sites:\n' - configs = [(name, site['router'], site['workdir']) for name, site in sites] - names, routers, dirs = zip(*configs) - print tables.Table(tables.Column('Name', names), - tables.Column('Router', routers), - tables.Column('Working dir', dirs)) - - -class Add(BaseOperation): - """Register uweb site""" - def ParseCall(self): - parser = OptionParser() - parser.add_option('-d', '--directory', action='store', - default='/', dest='directory') - parser.add_option('-u', '--update', action='store_true', - default=False, dest='update') - - opts, args = parser.parse_args() - return vars(opts), args - - def __call__(self, *name_router, **opts): - if len(name_router) != 2: - sys.exit(self.Help()) - sites = UwebSites() - name, router = name_router - directory = opts.get('directory', '/') - update = opts.get('update', False) - if name in sites and not update: - raise Error('Could not add a router with this name, one already exists.' - '\n\nTo update the existing, use the --update flag') - sites[name] = {'router': router, 'workdir': os.path.expanduser(directory)} - - def Help(self): - return ('Please provide a name and the module path for the router.\n' - 'Example: uweb add cookie_api cookies.router.api ' - '--directory="~/devel".') - - -class Remove(BaseOperation): - """Unregister uweb site""" - def ParseCall(self): - parser = OptionParser() - opts, args = parser.parse_args() - return vars(opts), args - - def __call__(self, *args): - if not args: - sys.exit(self.Help()) - try: - sites = UwebSites() - del sites[args[0]] - except ValueError: - raise Error('There was no site named %r' % args[0]) - - def Help(self): - return ('Please provide a name for the router to remove.\n' - 'Router names can be retrieved using the "list" command.') - - -# ############################################################################## -# Commands to control configured uWeb routers. -# -class Start(BaseOperation): - """Start project router""" - def ParseCall(self): - parser = OptionParser() - opts, args = parser.parse_args() - return vars(opts), args - - def __call__(self, *args): - if not args: - sys.exit(self.Help()) - site = UwebSites()[args[0]] - return subprocess.Popen(['python', '-m', site['router'], 'start'], - cwd=site['workdir']).wait() - - def Help(self): - return ('Please provide a name for the router to start.\n' - 'Router names can be retrieved using the "list" command.') - - -class Stop(BaseOperation): - """Stop project router""" - def ParseCall(self): - parser = OptionParser() - opts, args = parser.parse_args() - return vars(opts), args - - def __call__(self, *args): - if not args: - sys.exit(self.Help()) - site = UwebSites()[args[0]] - return subprocess.Popen(['python', '-m', site['router'], 'stop'], - cwd=site['workdir']).wait() - - def Help(self): - return ('Please provide a name for the router to stop.\n' - 'Router names can be retrieved using the "list" command.') - - -class Restart(BaseOperation): - """Restart project router""" - def ParseCall(self): - parser = OptionParser() - opts, args = parser.parse_args() - return vars(opts), args - - def __call__(self, *args): - if not args: - sys.exit(self.Help()) - site = UwebSites()[args[0]] - return subprocess.Popen(['python', '-m', site['router'], 'restart'], - cwd=site['workdir']).wait() - - def Help(self): - return ('Please provide a name for the router to restart.\n' - 'Router names can be retrieved using the "list" command.') - - -FUNCTIONS = {'init': Init, - 'genconf': GenerateApacheConfig, - 'list': ListSites, - 'add': Add, - 'remove': Remove, - 'start': Start, - 'restart': Restart, - 'stop': Stop} - - -def LongestImportPrefix(package): - candidates = [] - for path in sys.path: - if package.startswith(path + os.sep): - candidates.append(path) - print max(candidates, key=len) - - -def Help(): - return """uWeb management tool. - - Usage: `uweb COMMAND [options]` - - Project - init - Starts a new uWeb project with the given name - genconf - Generates an Apache configuration file - - Router management commands: - list - Lists all uWeb projects, their routers and working directories. - add - Adds a new project to the managed routers. - remove - Removes a project from the managed routers. - - Router control commands: - start - Starts a named router (as created with 'add'). - stop - Stops a named router (as created with 'add'). - restart - Convenience command to stop, and then start a router. - """ - - -def main(): - """Main uweb method""" - root_logger = logging.getLogger() - root_logger.setLevel(logging.DEBUG) - handler = logging.StreamHandler(sys.stdout) - root_logger.addHandler(handler) - - if len(sys.argv) < 2 or sys.argv[1] not in FUNCTIONS: - print Help() - sys.exit(1) - try: - FUNCTIONS[sys.argv[1]]().Run() - except Error, err_obj: - sys.exit('Error: %s' % err_obj) - except (IOError, OSError), err_obj: - sys.exit('I/O Error: %s' % err_obj) - -if __name__ == '__main__': - main() From 826d9046936153c5d9dbc63c88dca59988b3686a Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 27 Oct 2020 12:27:34 +0100 Subject: [PATCH 044/118] move test files to test folder outside of uweb package, remove some unneeded info from readme file, link to uweb3scaffold project for easy start of new projects --- README.md | 5 ----- {uweb3 => test}/test_model.py | 4 ++-- {uweb3 => test}/test_model_alchemy.py | 7 ++----- {uweb3 => test}/test_request.py | 0 {uweb3 => test}/test_templateparser.py | 0 5 files changed, 4 insertions(+), 12 deletions(-) rename {uweb3 => test}/test_model.py (99%) rename {uweb3 => test}/test_model_alchemy.py (98%) rename {uweb3 => test}/test_request.py (100%) rename {uweb3 => test}/test_templateparser.py (100%) diff --git a/README.md b/README.md index eb14d7a0..d4c8bc72 100644 --- a/README.md +++ b/README.md @@ -72,11 +72,6 @@ dev = True ``` This makes sure that µWeb3 restarts every time you modify something in the core of the framework aswell. -µWeb3 has inbuild XSRF protection. You can import it from uweb3.pagemaker.new_decorators checkxsrf. -This is a decorator and it will handle validation and generation of the XSRF. -The only thing you have to do is add the ```{{ xsrf [xsrf]}}``` tag into a form. -The xsrf token is accessible in any pagemaker with self.xsrf. - # Routing The default way to create new routes in µWeb3 is to create a folder called routes. In the routes folder create your pagemaker class of choice, the name doesn't matter as long as it inherits from PageMaker. diff --git a/uweb3/test_model.py b/test/test_model.py similarity index 99% rename from uweb3/test_model.py rename to test/test_model.py index 75f08cb4..1cc44522 100755 --- a/uweb3/test_model.py +++ b/test/test_model.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 """Test suite for the database abstraction module (model).""" # Too many public methods @@ -8,7 +8,7 @@ import unittest # Importing uWeb3 makes the SQLTalk library available as a side-effect -from uweb3.ext_lib.libs.sqltalk import mysql +from uweb3.libs.sqltalk import mysql # Unittest target from uweb3 import model from pymysql.err import InternalError diff --git a/uweb3/test_model_alchemy.py b/test/test_model_alchemy.py similarity index 98% rename from uweb3/test_model_alchemy.py rename to test/test_model_alchemy.py index 5f7ecb6c..063f728a 100755 --- a/uweb3/test_model_alchemy.py +++ b/test/test_model_alchemy.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 """Test suite for the database abstraction module (model).""" # Too many public methods @@ -6,11 +6,8 @@ # Standard modules import unittest -from contextlib import contextmanager -import pymysql import sqlalchemy -from pymysql.err import InternalError from sqlalchemy import (Column, ForeignKey, Integer, MetaData, String, Table, create_engine) from sqlalchemy.exc import IntegrityError, OperationalError @@ -19,7 +16,7 @@ import uweb3 from uweb3.alchemy_model import AlchemyRecord -from uweb3.ext_lib.libs.sqltalk import mysql +from uweb3.libs.sqltalk import mysql # ############################################################################## # Record classes for testing diff --git a/uweb3/test_request.py b/test/test_request.py similarity index 100% rename from uweb3/test_request.py rename to test/test_request.py diff --git a/uweb3/test_templateparser.py b/test/test_templateparser.py similarity index 100% rename from uweb3/test_templateparser.py rename to test/test_templateparser.py From 17d80cff8b219ff5f8793a4d0cc7a08bae38e7f8 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 27 Oct 2020 12:28:01 +0100 Subject: [PATCH 045/118] forgot to include the readme file --- README.md | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index d4c8bc72..6b062e31 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,9 @@ python3 setup.py install # Or you can install in development mode which allows easy modification of the source: python3 setup.py develop --user -cd uweb3/scaffold +# clone the uweb3scaffold project to get started +git clone git@github.com:underdarknl/uweb3scaffold.git +cd uweb3scaffold python3 serve.py ``` @@ -83,18 +85,21 @@ After creating your pagemaker be sure to add the route endpoint to routes list i - A classmethod called loadModules that loads all pagemaker modules inheriting from PageMaker class - A XSRF class - Generates a xsrf token and creates a cookie if not in place - - Validates the xsrf token in a post request if the enable_xsrf flag is set in the config.ini - In requests: - Self.method attribute - self.post.form attribute. This is the post request as a dict, includes blank values. - - Method called Redirect #Moved from the response class to the request class so cookies that are set before a redirect are actually set. + - Method called Redirect #Moved from the response class to the request class so cookies that are set before a redirect are actually persist to the next request. - Method called DeleteCookie - - A if statement that checks string like cookies and raises an error if the size is equal or bigger than 4096 bytes. - - AddCookie method, edited this and the response class to handle the setting of multiple cookies. Previously setting multiple cookies with the Set-Cookie header would make the last cookie the only cookie. + - An if statement that checks string like cookies and raises an error if the size is equal or bigger than 4096 bytes. + - AddCookie method, now supports multiple calls to Set-Cookie setting all cookies instead of just the last. - In pagemaker/decorators: - Loggedin decorator that validates if user is loggedin based on cookie with userid - Checkxsrf decorator that checks if the incorrect_xsrf_token flag is set - In templatepaser: - - A function called _TemplateConstructXsrf that generates a hidden input field with the supplied value: {{ xsrf [xsrf_variable]}} -- In libs/sqltalk - - So far so good but it might crash on functions that I didn't use yet + - Its possible to register tags to the parser, for example in your _postInit call + - Its possible to register 'Just in Time' tags to the parser, which will be evaluated only when needed. +- In libs/sqltalk, use of PyMysql instead of c mysql functions +- Connections + - All Connections are now all availabe on the self.connections member of the pagemaker, regardless of what type of backend they connect to + - Cookies (signed and safe) are available as a connection + - Config files (read/write) are available as a connection From f787732978da42f54066300d0c91b0ba6c7f8f3b Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 27 Oct 2020 14:37:05 +0100 Subject: [PATCH 046/118] urlsplitter is not needed in core uweb3 package --- uweb3/libs/urlsplitter/__init__.py | 47 ---------------------------- uweb3/libs/urlsplitter/test.py | 49 ------------------------------ 2 files changed, 96 deletions(-) delete mode 100644 uweb3/libs/urlsplitter/__init__.py delete mode 100644 uweb3/libs/urlsplitter/test.py diff --git a/uweb3/libs/urlsplitter/__init__.py b/uweb3/libs/urlsplitter/__init__.py deleted file mode 100644 index f9d91af5..00000000 --- a/uweb3/libs/urlsplitter/__init__.py +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/python3 -"""This module is used to split an url into a dict and simultaneously validates -if the url is valid or not. - -Every url provided should start with http:// or https:// otherwise it is seen as invalid. -""" - -__author__ = 'Stef van Houten (stefvanhouten@gmail.com)' -__version__ = 0.1 - -import re - -from tld import get_tld - -def split_url(url): - # If fail_silently is True return None if url suffix is invalid - if not isinstance(url, str): - raise Exception("Url should be a string") - - suffix = get_tld(url, fail_silently=True) - - if not suffix: - # TODO: What should the function return on a invalid url suffix? - raise Exception("Url not valid") - - # Get the leftovers after the suffix - route = url[url.find(suffix) + len(suffix):] - - # Use regex to filter out the https:// or www. - regex = re.compile(r"https?://(www\.)?") - domain = regex.sub('', url).strip().strip('/')[:-(len(route) + len(suffix))] - - url_type = url[:url.find(domain)] - - # Remove trailing dot if there is any - if domain[-1] == ".": - domain = domain[:-1] - - if url_type[-1] == ".": - url_type = url_type[:-1] - - return { - 'type': url_type, - 'domain': domain, - 'suffix': suffix, - 'route': target, - } diff --git a/uweb3/libs/urlsplitter/test.py b/uweb3/libs/urlsplitter/test.py deleted file mode 100644 index 7570148e..00000000 --- a/uweb3/libs/urlsplitter/test.py +++ /dev/null @@ -1,49 +0,0 @@ -import unittest -from __init__ import split_url - -class testUrlSplitter(unittest.TestCase): - - def testValidUrl(self): - """See if we correctly clean up header injection attemps""" - self.assertEqual(split_url('https://test.test.com'), { - 'type': 'https://', - 'domain': 'test.test', - 'suffix': 'com', - 'target': '' - }) - - self.assertEqual(split_url('https://test.test.com/someurl'), { - 'type': 'https://', - 'domain': 'test.test', - 'suffix': 'com', - 'target': '/someurl' - }) - - self.assertEqual(split_url('https://www.google.co.uk'), { - 'type': 'https://www', - 'domain': 'google', - 'suffix': 'co.uk', - 'target': '' - }) - - self.assertEqual( split_url('https://www.test.com.nl.de'), { - 'type': 'https://www', - 'domain': 'test.com.nl', - 'suffix': 'de', - 'target': '' - }) - - - def testInvalidUrl(self): - message = 'Url not valid' - with self.assertRaises(Exception) as context: - split_url('www.google.com') - self.assertTrue(message in str(context.exception)) - - with self.assertRaises(Exception) as context: - split_url(' ') - self.assertTrue(message in str(context.exception)) - - -if __name__ == '__main__': - unittest.main() \ No newline at end of file From 5643162b41294b682c3bf2024cbee348db033cf5 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 27 Oct 2020 14:47:01 +0100 Subject: [PATCH 047/118] remove unneeded references, remove admin module for now --- DEVELOPMENT | 5 +- MANIFEST.in | 6 +- setup.py | 12 +- uweb3/pagemaker/admin.py | 215 ------------------------------ uweb3/pagemaker/admin/edit.html | 23 ---- uweb3/pagemaker/admin/index.html | 188 -------------------------- uweb3/pagemaker/admin/record.html | 21 --- uweb3/pagemaker/decorators.py | 14 +- 8 files changed, 18 insertions(+), 466 deletions(-) delete mode 100644 uweb3/pagemaker/admin.py delete mode 100644 uweb3/pagemaker/admin/edit.html delete mode 100644 uweb3/pagemaker/admin/index.html delete mode 100644 uweb3/pagemaker/admin/record.html diff --git a/DEVELOPMENT b/DEVELOPMENT index 3461ce5f..7625b72e 100644 --- a/DEVELOPMENT +++ b/DEVELOPMENT @@ -5,7 +5,10 @@ You can setup a working development environment by issueing: ```bash python3 setup.py develop --user -cd uweb3/scaffold + +# clone the uweb3scaffold project to get started +git clone git@github.com:underdarknl/uweb3scaffold.git +cd uweb3scaffold python3 serve.py ``` diff --git a/MANIFEST.in b/MANIFEST.in index 1217d068..932be389 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include README.md LICENSE -recursive-include uweb3 *.js *.css *.html *.conf *.txt *.utp -recursive-include uweb3/ext_lib *.py +include README.md LICENSE DEVELOPMENT CONTRIBUTORS +recursive-include uweb3 *.html *.conf *.py +recursive-include test *.py diff --git a/setup.py b/setup.py index 69384f84..03ac5611 100644 --- a/setup.py +++ b/setup.py @@ -5,12 +5,8 @@ from setuptools import setup, find_packages REQUIREMENTS = [ - 'decorator', 'PyMySQL', - 'python-magic', - 'pytz', - 'simplejson', - 'bcrypt' + 'pytz' ] # 'sqlalchemy', @@ -28,7 +24,7 @@ def version(): setup( - name='uWeb3 test', + name='uWeb3', version=version(), description='uWeb, python3, uswgi compatible micro web platform', long_description_file = 'README.md', @@ -42,8 +38,8 @@ def version(): ], author='Jan Klopper', author_email='jan@underdark.nl', - url='https://github.com/underdark.nl/uWeb3', - keywords='minimal web framework', + url='https://github.com/underdark.nl/uweb3', + keywords='minimal python web framework', packages=find_packages(), include_package_data=True, zip_safe=False, diff --git a/uweb3/pagemaker/admin.py b/uweb3/pagemaker/admin.py deleted file mode 100644 index 163238c1..00000000 --- a/uweb3/pagemaker/admin.py +++ /dev/null @@ -1,215 +0,0 @@ -#!/usr/bin/python -"""uWeb3 PageMaker Mixins for admin purposes.""" - -# Standard modules -import datetime -import decimal -import inspect -import os - -# Package modules -from .. import model -from .. import templateparser - -NOT_ALLOWED_METHODS = dir({}) + ['key', 'identifier'] - -FIELDTYPES = {'datetime': datetime.datetime, - 'decimal': decimal.Decimal} - -class AdminMixin(object): - """Provides an admin interface based on the available models""" - - def _Admin(self, url): - self.parser.RegisterFunction('classname', lambda cls: type(cls).__name__) - - if not self.ADMIN_MODEL: - return 'Setup ADMIN_MODEL first' - indextemplate = templateparser.FileTemplate( - os.path.join(os.path.dirname(__file__), 'admin', 'index.html')) - - urlparts = (url or '').split('/') - table = None - method = 'List' - methods = None - results = None - columns = None - basepath = self.__BasePath() - resultshtml = [] - columns = None - edithtml = None - message = None - docs = None - if len(urlparts) > 2: - if urlparts[1] == 'table': - table = urlparts[2] - methods = self.__AdminTablesMethods(table) - docs = self.__GetClassDocs(table) - if len(urlparts) > 3: - method = urlparts[3] - if method == 'edit': - edithtml = self.__EditRecord(table, urlparts[4]) - elif method == 'delete': - key = self.post.getfirst('key') - if self.__DeleteRecord(table, key): - message = '%s with key %s deleted.' %(table, key) - else: - message = 'Could not find %s with key %s.' %(table, key) - elif method == 'save': - message = self.__SaveRecord(table, self.post.getfirst('key')) - else: - (columns, results) = self.__AdminTablesMethodsResults(urlparts[2], - method) - - resulttemplate = templateparser.FileTemplate( - os.path.join(os.path.dirname(__file__), 'admin', 'record.html')) - - for result in results: - resultshtml.append(resulttemplate.Parse(result=result['result'], - key=result['key'], - table=table, - basepath=basepath, - fieldtypes=FIELDTYPES)) - elif urlparts[1] == 'method': - table = urlparts[2] - methods = self.__AdminTablesMethods(table) - docs = self.__GetDocs(table, urlparts[3]) - return indextemplate.Parse(basepath=basepath, - tables=self.__AdminTables(), - table=table, - columns=columns, - method=method, - methods=methods, - results=resultshtml, - edit=edithtml, - message=message, - docs=docs) - - def __GetDocs(self, table, method): - if self.__CheckTable(table): - table = getattr(self.ADMIN_MODEL, table) - methodobj = getattr(table, method) - if methodobj.__doc__: - return inspect.cleandoc(methodobj.__doc__) - try: - while table: - table = table.__bases__[0] - methodobj = getattr(table, method) - if methodobj.__doc__: - return inspect.cleandoc(methodobj.__doc__) - except AttributeError: - pass - return 'No documentation avaiable' - - def __GetClassDocs(self, table): - if self.__CheckTable(table): - table = getattr(self.ADMIN_MODEL, table) - if table.__doc__: - return inspect.cleandoc(table.__doc__) - try: - while table: - table = table.__bases__[0] - if table.__doc__: - return inspect.cleandoc(table.__doc__) - except AttributeError: - pass - return 'No documentation avaiable' - - def __EditRecord(self, table, key): - self.parser.RegisterFunction('classname', lambda cls: type(cls).__name__) - edittemplate = templateparser.FileTemplate( - os.path.join(os.path.dirname(__file__), 'admin', 'edit.html')) - fields = self.__EditRecordFields(table, key) - if not fields: - return 'Could not load record with %s' % key - return edittemplate.Parse(table=table, - key=key, - basepath=self.__BasePath(), - fields=fields, - fieldtypes=FIELDTYPES) - - def __SaveRecord(self, table, key): - if self.__CheckTable(table): - table = getattr(self.ADMIN_MODEL, table) - try: - obj = table.FromPrimary(self.connection, key) - except model.NotExistError: - return 'Could not load record with %s' % key - for item in obj.keys(): - if (isinstance(obj[item], int) or - isinstance(obj[item], long)): - obj[item] = int(self.post.getfirst(item, 0)) - elif (isinstance(obj[item], float) or - isinstance(obj[item], decimal.Decimal)): - obj[item] = float(self.post.getfirst(item, 0)) - elif isinstance(obj[item], basestring): - obj[item] = self.post.getfirst(item, '') - elif isinstance(obj[item], datetime.datetime): - obj[item] = self.post.getfirst(item, '') - else: - obj[item] = int(self.post.getfirst(item, 0)) - try: - obj.Save() - except Exception as error: - return error - return 'Changes saved' - return 'Invalid table' - - def __DeleteRecord(self, table, key): - if self.__CheckTable(table): - table = getattr(self.ADMIN_MODEL, table) - try: - obj = table.FromPrimary(self.connection, key) - obj.Delete() - return True - except model.NotExistError: - return False - return False - - def __BasePath(self): - return self.req.path.split('/')[1] - - def __EditRecordFields(self, table, key): - if self.__CheckTable(table): - table = getattr(self.ADMIN_MODEL, table) - try: - return table.FromPrimary(self.connection, key) - except model.NotExistError: - return False - return False - - def __CheckTable(self, table): - """Verfies the given name is that of a model.BaseRecord subclass.""" - tableclass = getattr(self.ADMIN_MODEL, table) - return type(tableclass) == type and issubclass(tableclass, model.Record) - - def __AdminTables(self): - tables = [] - for table in dir(self.ADMIN_MODEL): - if self.__CheckTable(table): - tables.append(table) - return tables - - def __AdminTablesMethods(self, table): - if self.__CheckTable(table): - table = getattr(self.ADMIN_MODEL, table) - methods = [] - for method in dir(table): - if (not method.startswith('_') - and method not in NOT_ALLOWED_METHODS - and callable(getattr(table, method))): - methods.append(method) - return methods - return False - - def __AdminTablesMethodsResults(self, tablename, methodname='List'): - if self.__CheckTable(tablename): - table = getattr(self.ADMIN_MODEL, tablename) - method = getattr(table, methodname) - results = method(self.connection) - resultslist = [] - for result in results: - resultslist.append({'result': result.values(), - 'key': result.key}) - if resultslist: - return result.keys(), resultslist - return (), () diff --git a/uweb3/pagemaker/admin/edit.html b/uweb3/pagemaker/admin/edit.html deleted file mode 100644 index 639a24b1..00000000 --- a/uweb3/pagemaker/admin/edit.html +++ /dev/null @@ -1,23 +0,0 @@ -

Editing: [table], key: [key]

-
- -
    -{{ for key, value in [fields|items] }} -
  • - {{ if isinstance([value], basestring) }} - - {{ elif isinstance([value], int) or isinstance([value], long) or isinstance([value], float) or isinstance([value], [fieldtypes:decimal]) }} - - {{ elif isinstance([value], [fieldtypes:datetime]) }} - - {{ elif not [value] }} - - {{ else }} - - [value:key] ([value|classname]) - {{ endif }} -
  • -{{ endfor }} -
  • -
-
diff --git a/uweb3/pagemaker/admin/index.html b/uweb3/pagemaker/admin/index.html deleted file mode 100644 index db05bf0a..00000000 --- a/uweb3/pagemaker/admin/index.html +++ /dev/null @@ -1,188 +0,0 @@ - - - - - - µWeb, Admin interface {{if [table]}}[table]{{ endif }} {{if [method]}} - [method]{{ endif }} - - - -
- {{ if [results] }} -

Results for [table] / [method]

- - - - - {{ for column in [columns]}} - - {{ endfor }} - - - - - - {{ for result in [results] }} - [result|raw] - {{ endfor }} - -
[column|raw]EditDelete
- {{ elif [edit]}} - [edit|raw] - {{ elif [message]}} - [message] - {{ endif }} - {{ if [docs]}} -

Documentation {{ if not [results] and not [edit]}} for [method] on [table] {{ endif }}

-
[docs]
- {{ endif }} -
- - - diff --git a/uweb3/pagemaker/admin/record.html b/uweb3/pagemaker/admin/record.html deleted file mode 100644 index 66fbe460..00000000 --- a/uweb3/pagemaker/admin/record.html +++ /dev/null @@ -1,21 +0,0 @@ - - - {{ for column in [result] }} - {{ if isinstance([column], basestring) }} - [column] - {{ elif isinstance([column], int) or isinstance([column], long) or isinstance([column], float) or isinstance([column], [fieldtypes:decimal]) }} - [column] - {{ elif isinstance([column], [fieldtypes:datetime]) }} - [column] - {{ elif not [column] }} - None - {{ else }} - [column:key] - {{ endif }} - {{ endfor }} - Edit -
- - -
- diff --git a/uweb3/pagemaker/decorators.py b/uweb3/pagemaker/decorators.py index 495ecc0a..07137a03 100644 --- a/uweb3/pagemaker/decorators.py +++ b/uweb3/pagemaker/decorators.py @@ -5,7 +5,7 @@ import hashlib import pickle import pytz -import simplejson +import json import time import uweb3 @@ -80,8 +80,8 @@ def wrapper(*args, **kwargs): data = handler.FromSignature(args[0].connection, maxage, name, modulename, - simplejson.dumps(args[1:]), - simplejson.dumps(kwargs)) + json.dumps(args[1:]), + json.dumps(kwargs)) if verbose: data = '%s' % ( pickle.loads(codecs.decode(data['data'].encode(), "base64")), @@ -97,8 +97,8 @@ def wrapper(*args, **kwargs): data = handler.FromSignature(args[0].connection, maxage, name, modulename, - simplejson.dumps(args[1:]), - simplejson.dumps(kwargs)) + json.dumps(args[1:]), + json.dumps(kwargs)) break except Exception: sleep = min(sleep*2, maxsleepinterval) @@ -117,8 +117,8 @@ def wrapper(*args, **kwargs): cache = handler.Create(args[0].connection, { 'name': name, 'modulename': modulename, - 'args': simplejson.dumps(args[1:]), - 'kwargs': simplejson.dumps(kwargs), + 'args': json.dumps(args[1:]), + 'kwargs': json.dumps(kwargs), 'creating': now, 'created': now }) From a6579b8089161b37ce5c1318e7f2e96c78b256d1 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 27 Oct 2020 16:45:09 +0100 Subject: [PATCH 048/118] fix sqlite results to function with new resultset handling --- README.md | 2 +- uweb3/libs/sqltalk/sqlite/cursor.py | 5 +++-- uweb3/libs/sqltalk/sqlresult.py | 3 +-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 6b062e31..2db07def 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ Since µWeb inception we have used it for many projects, and while it did its jo The following example applications for uWeb3 exist: -* [uWeb3-info](https://github.com/edelooff/uWeb3-info): This demonstrates most µWeb3 features, and gives you examples on how to use most of them. +* [uWeb3-scaffold](https://github.com/underdarknl/uweb3scaffold): This is an empty project which you can fork to start your own website * [uWeb3-logviewer](https://github.com/edelooff/uWeb3-logviewer): This allows you to view and search in the logs generated by all µWeb and µWeb3 applications. # µWeb3 installation diff --git a/uweb3/libs/sqltalk/sqlite/cursor.py b/uweb3/libs/sqltalk/sqlite/cursor.py index 440ea0ba..54e63898 100644 --- a/uweb3/libs/sqltalk/sqlite/cursor.py +++ b/uweb3/libs/sqltalk/sqlite/cursor.py @@ -22,13 +22,14 @@ def Execute(self, query, args=(), many=False): except Exception: self.connection.logger.exception('Exception during query execution') raise + fieldnames = list(field[0] for field in result.description) return sqlresult.ResultSet( affected=result.rowcount, charset='utf-8', - fields=result.description, + fields=fieldnames, insertid=result.lastrowid, query=(query, tuple(args)), - result=result.fetchall()) + result=list(dict(zip(fieldnames, row)) for row in result.fetchall())) def Insert(self, table, values): if not values: diff --git a/uweb3/libs/sqltalk/sqlresult.py b/uweb3/libs/sqltalk/sqlresult.py index 35e4fc31..968ba700 100644 --- a/uweb3/libs/sqltalk/sqlresult.py +++ b/uweb3/libs/sqltalk/sqlresult.py @@ -181,8 +181,7 @@ class ResultSet(object): @ charset - str Character set used for this connection. @ fields - tuple - Fields in the ResultSet. Each field is a tuple of 7 elements as specified - by the Python DB API (v2). + Fields in the ResultSet. @ insertid - int Auto-increment ID that was generated upon the last insert. @ query - str From 422714bddc79f1d8073325711d0821d99a6b35df Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 28 Oct 2020 13:25:14 +0100 Subject: [PATCH 049/118] make the initial settingsmanager more configurable, and only reread configs when they are changed on disk. --- uweb3/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index b35f17f9..8775a4c7 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -166,9 +166,9 @@ class uWeb(object): Returns: RequestHandler: Configured closure that is ready to process requests. """ - def __init__(self, page_class, routes, executing_path=None): + def __init__(self, page_class, routes, executing_path=None, config='config'): self.executing_path = executing_path if executing_path else os.path.dirname(__file__) - self.config = SettingsManager(filename='config', executing_path=self.executing_path) + self.config = SettingsManager(filename=config, path=self.executing_path) self.logger = self.setup_logger() self.inital_pagemaker = page_class self.registry = Registry() From c874a604b744a5c1d1d652147789861de7c8ee8c Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 28 Oct 2020 13:25:18 +0100 Subject: [PATCH 050/118] make the initial settingsmanager more configurable, and only reread configs when they are changed on disk. --- uweb3/model.py | 51 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 9 deletions(-) diff --git a/uweb3/model.py b/uweb3/model.py index a4a289aa..d97c3e0b 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -37,29 +37,50 @@ class PermissionError(Error): class SettingsManager(object): - def __init__(self, filename=None, executing_path=None): + def __init__(self, filename=None, path=None): """Creates a ini file with the child class name Arguments: - % filename: str - Name of the file without the extension + % filename: str, optional + Name of the file, optionally without the extension will default to .ini + If not filename is given the class.__name__ will be used to look for the config file in the path + % path: str, Optional + Path to the config file, will be used if filename is relative, eg does not start with '/' """ self.options = None + extension = '' if filename and filename.endswith(('.ini', '.conf')) else '.ini' if filename: - self.FILENAME = f"{filename[:1].lower() + filename[1:]}.ini" + self.FILENAME = f"{filename[:1].lower() + filename[1:] + extension}" else: - self.FILENAME = f"{self.__class__.__name__[:1].lower() + self.__class__.__name__[1:]}.ini" + self.FILENAME = self.TableName() + extension - self.FILE_LOCATION = os.path.join(executing_path, self.FILENAME) + if path and not filename.startswith('/'): + self.FILE_LOCATION = os.path.join(path, self.FILENAME) + else: + self.FILE_LOCATION = self.FILENAME self.__CheckPermissions() - if not os.path.isfile(self.FILE_LOCATION): os.mknod(self.FILE_LOCATION) + self.mtime = None self.config = configparser.ConfigParser() self.Read() + @classmethod + def TableName(cls): + """Returns the 'database' table name for the SettingsManager class. + + If this is not explicitly defined by the class constant `_TABLE`, the return + value will be the class name with the first letter lowercased. + We stick to the same naming scheme as for more table like connectors even + though we use files instead of tables in this class. + """ + if cls._TABLE: + return cls._TABLE + name = cls.__name__ + return name[0].lower() + name[1:] + def __CheckPermissions(self): """Checks if SettingsManager can read/write to file.""" if not os.path.isfile(self.FILE_LOCATION): @@ -90,10 +111,19 @@ def Create(self, section, key, value): self.config.set(section, key, value) self._Write(False) + self.mtime = None def Read(self): - self.config.read(self.FILE_LOCATION) - self.options = self.config._sections + """Reads the config file and populates the options member + It uses the mtime to see if any re-reading is required""" + if not self.mtime: + curtime = os.path.getmtime(self.FILE_LOCATION) + if self.mtime and self.mtime == curtime: + return False + self.config.read(self.FILE_LOCATION) + self.options = self.config._sections + self.mtime = curtime + return True def Update(self, section, key, value): """Updates ini file @@ -111,6 +141,7 @@ def Update(self, section, key, value): self.config.add_section(section) self.config.set(section, key, value) self._Write() + self.mtime = None def Delete(self, section, key=None): """Delete sections/keys from the INI file @@ -131,6 +162,8 @@ def Delete(self, section, key=None): if not key: self.config.remove_section(section) self._Write() + self.mtime = None + return True def _Write(self, reread=True): """Internal function to store the current config to file""" From 50b93e50258f1ef65d7cc738b3ca9e54bb9126b0 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 28 Oct 2020 13:26:15 +0100 Subject: [PATCH 051/118] update setup file to reflect changes since uweb on python2 --- setup.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 03ac5611..62f70075 100644 --- a/setup.py +++ b/setup.py @@ -31,10 +31,12 @@ def version(): long_description_content_type = 'text/markdown', license='ISC', classifiers=[ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', + 'Environment :: Web Environment', 'License :: OSI Approved :: ISC License (ISCL)', 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python :: 3', ], author='Jan Klopper', author_email='jan@underdark.nl', From a486e95bd3318f7ef759e5d2260ba3ad4113646e Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 29 Oct 2020 17:53:31 +0100 Subject: [PATCH 052/118] remove unneeded imports --- uweb3/model.py | 1 - uweb3/response.py | 1 - 2 files changed, 2 deletions(-) diff --git a/uweb3/model.py b/uweb3/model.py index d97c3e0b..e325400f 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -4,7 +4,6 @@ # Standard modules import os import datetime -import simplejson import sys import hashlib import pickle diff --git a/uweb3/response.py b/uweb3/response.py index ca339820..816a4593 100644 --- a/uweb3/response.py +++ b/uweb3/response.py @@ -10,7 +10,6 @@ import json from collections import defaultdict -from .libs.safestring import JSONsafestring class Response(object): """Defines a full HTTP response. From 6092e2cbe29b6a58b809e986a7a720f29a30b58f Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 30 Oct 2020 13:44:02 +0100 Subject: [PATCH 053/118] fix curtime not being set on unread config files --- uweb3/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uweb3/model.py b/uweb3/model.py index e325400f..56654b4d 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -121,7 +121,7 @@ def Read(self): return False self.config.read(self.FILE_LOCATION) self.options = self.config._sections - self.mtime = curtime + self.mtime = curtime return True def Update(self, section, key, value): From 190a322150ab7fc5ec5bff6d4021a0d682676758 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 2 Nov 2020 17:37:53 +0100 Subject: [PATCH 054/118] allow <= and >= in eval --- uweb3/templateparser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index 2f11aca0..6a6279d3 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -110,8 +110,8 @@ def values(self): "isinstance": isinstance, **{key: value for (key,value) in vars(math).items() if not key.startswith('__')}}, 'operators': (ast.Module, ast.Expr, ast.Load, ast.Expression, ast.Add, ast.And, - ast.Sub, ast.UnaryOp, ast.Num, ast.BinOp, ast.Mult, ast.Gt, - ast.Div, ast.Pow, ast.BitOr, ast.BitAnd, ast.BitXor, ast.Lt, + ast.Sub, ast.UnaryOp, ast.Num, ast.BinOp, ast.Mult, ast.Gt, ast.GtE, + ast.Div, ast.Pow, ast.BitOr, ast.BitAnd, ast.BitXor, ast.Lt, ast.LtE, ast.USub, ast.UAdd, ast.FloorDiv, ast.Mod, ast.LShift, ast.RShift, ast.Invert, ast.Call, ast.Name, ast.Compare, ast.Eq, ast.NotEq, ast.Not, ast.Or, ast.BoolOp, ast.Str)} From dff8249fecb866432e162ab7bc8ef84134de916b Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 11 Nov 2020 16:47:14 +0100 Subject: [PATCH 055/118] shuffle some things around for safestrings, including integration with the templateparser, add join method to safestrings and use it, add unittests for joins on safestrings and joins --- test/test_templateparser.py | 13 +++++++++++++ uweb3/libs/safestring/__init__.py | 6 ++++++ uweb3/libs/safestring/test.py | 14 ++++++++++++++ uweb3/templateparser.py | 3 ++- 4 files changed, 35 insertions(+), 1 deletion(-) diff --git a/test/test_templateparser.py b/test/test_templateparser.py index acb6f296..9e3f19aa 100755 --- a/test/test_templateparser.py +++ b/test/test_templateparser.py @@ -107,6 +107,18 @@ def testSingleTagTemplate(self): result = self.tmpl(template).Parse(single='just one') self.assertEqual(result, 'Template with just one tag') + def testSaveTagTemplate(self): + """[BasicTag] Templates with basic tags get returned properly when replacement is already html safe""" + template = 'Template with just [single] tag' + result = self.tmpl(template).Parse(single=templateparser.HTMLsafestring('a safe')) + self.assertEqual(result, 'Template with just a safe tag') + + def testUnsaveTagTemplate(self): + """[BasicTag] Templates with basic tags get returned properly when replacement is not html safe""" + template = 'Template with just [single] tag' + result = self.tmpl(template).Parse(single='an unsafe') + self.assertEqual(result, 'Template with just <b>an unsafe</b> tag') + def testCasedTag(self): """[BasicTag] Tag names are case-sensitive""" template = 'The parser has no trouble with [cAsE] [case].' @@ -390,6 +402,7 @@ def testTagFunctionLen(self): template = '[numbers|len]' self.assertEqual(self.parse(template, numbers=range(12)), "12") + class TemplateTagFunctionClosures(unittest.TestCase): """Tests the functions that are performed on replaced tags.""" @staticmethod diff --git a/uweb3/libs/safestring/__init__.py b/uweb3/libs/safestring/__init__.py index 1e6ca39e..d9e37d45 100644 --- a/uweb3/libs/safestring/__init__.py +++ b/uweb3/libs/safestring/__init__.py @@ -86,6 +86,12 @@ def escape(self, data): def unescape(self, data): raise NotImplementedError + def join(self, items): + output = [] + for item in items: + output.append(self.__upgrade__(item)) + return self.__class__(''.join(output)) + class SQLSAFE(Basesafestring): CHARS_ESCAPE_DICT = { diff --git a/uweb3/libs/safestring/test.py b/uweb3/libs/safestring/test.py index 1fba185e..b7f9db3d 100755 --- a/uweb3/libs/safestring/test.py +++ b/uweb3/libs/safestring/test.py @@ -70,6 +70,20 @@ def test_format_keyword(self): testdata = HTMLsafestring('foo {kw} test').format(kw='') self.assertEqual(testdata, 'foo <b> test') + def test_join(self): + """Tests to join two already safe list items""" + testdata = HTMLsafestring('').join((HTMLsafestring(''), + HTMLsafestring(''))) + self.assertEqual(testdata, '') + self.assertIsInstance(testdata, HTMLsafestring) + + def test_join_unsafe(self): + """Test a join over possibly insafe and safe strings combined""" + testdata = HTMLsafestring('').join(('', + HTMLsafestring(''))) + self.assertEqual(testdata, '<b>') + self.assertIsInstance(testdata, HTMLsafestring) + class TestJSonStringMethods(unittest.TestCase): def test_addition(self): diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index 6a6279d3..bc4a35c0 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -398,7 +398,7 @@ def Parse(self, returnRawTemplate=False, **kwds): The template is parsed by parsing each of its members and combining that. """ - htmlsafe = HTMLsafestring(''.join(tag.Parse(**kwds) for tag in self)) + htmlsafe = HTMLsafestring('').join(HTMLsafestring(tag.Parse(**kwds)) for tag in self) htmlsafe.content_hash = hashlib.md5(htmlsafe.encode()).hexdigest() if returnRawTemplate: raw = HTMLsafestring(self) @@ -1056,6 +1056,7 @@ def LimitedEval(expr, astvisitor, evallocals = {}): TAG_FUNCTIONS = { 'default': lambda d: HTMLsafestring('') + d, 'html': lambda d: HTMLsafestring('') + d, + 'htmlsource': lambda d: HTMLsafestring(d, unsafe=True), 'raw': lambda d: Unsafestring(d), 'url': lambda d: HTMLsafestring(URLqueryargumentsafestring(d, unsafe=True)), 'type': type, From 36700d22edaf3a6bd6674ccadf21ea6af74443b2 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 23 Nov 2020 10:20:22 +0100 Subject: [PATCH 056/118] add distinct functionality to List method, add as to List and Select methods, make breaking connections return more descriptive output --- uweb3/connections.py | 4 ++-- uweb3/libs/sqltalk/mysql/connection.py | 23 +++++++++++++++------ uweb3/libs/sqltalk/mysql/cursor.py | 28 ++++++++++++++++++++------ uweb3/model.py | 6 ++++-- 4 files changed, 45 insertions(+), 16 deletions(-) diff --git a/uweb3/connections.py b/uweb3/connections.py index b42465ce..ee9bfc55 100644 --- a/uweb3/connections.py +++ b/uweb3/connections.py @@ -77,8 +77,8 @@ def RelevantConnection(self, level=2): self.__connections[con_type] = self.__connectors[con_type]( self.config, self.options, request, self.debug) return self.__connections[con_type].connection - except KeyError: - raise TypeError('No connector for: %r, available: %r' % (con_type, self.__connectors)) + except KeyError as error: + raise TypeError('No connector for: %r, available: %r, %r' % (con_type, self.__connectors, error)) def __enter__(self): """Proxies the transaction to the underlying relevant connection.""" diff --git a/uweb3/libs/sqltalk/mysql/connection.py b/uweb3/libs/sqltalk/mysql/connection.py index 074156b4..d5140e25 100644 --- a/uweb3/libs/sqltalk/mysql/connection.py +++ b/uweb3/libs/sqltalk/mysql/connection.py @@ -198,13 +198,20 @@ def CurrentDatabase(self): """Return the name of the currently used database""" return self.Query('SELECT DATABASE()')[0] - def EscapeField(self, field): - """Returns a SQL escaped field or table name.""" + def EscapeField(self, field, multiple=False): + """Returns a SQL escaped field or table name. + + Set multiple = True if field is a tuple of names to be escaped. + If multiple = False, and a tuple is encountered `field` as `name` will be + returned where the second part of the tuple is the `name` part. + """ if not field: return '' if isinstance(field, str): fields = '.'.join('`%s`' % f.replace('`', '``') for f in field.split('.')) return fields.replace('`*`', '*') + elif not multiple and isinstance(field, tuple): + return '%s as %s' % (self.EscapeField(field[0]), self.EscapeField(field[1])) return map(self.EscapeField, field) def EscapeValues(self, obj): @@ -221,12 +228,16 @@ def Info(self): """Returns a dictionary of MySQL server info and current active database. Returns - dictionary: keys: 'db', 'charset', 'server' + dictionary: keys: 'db', 'charset', 'server', 'debug', 'autocommit', + 'querycount', 'transactioncount' """ - # TODO(Elmer): Make this return more useful information and statistics return {'db': self.CurrentDatabase(), - 'charset': self.charset, - 'server': self.ServerInfo()} + 'charset': self._GetCharacterSet(), + 'server': self.ServerInfo(), + 'debug': self.debug, + 'autocommit': self.autocommit_mode, + 'querycount': self.counter_queries, + 'transactioncount': self.counter_transactions} def Query(self, query_string, cur=None): self.counter_queries += 1 diff --git a/uweb3/libs/sqltalk/mysql/cursor.py b/uweb3/libs/sqltalk/mysql/cursor.py index 7b826430..341fbbde 100644 --- a/uweb3/libs/sqltalk/mysql/cursor.py +++ b/uweb3/libs/sqltalk/mysql/cursor.py @@ -77,8 +77,7 @@ def _StringFields(fields, field_escape): return '*' elif isinstance(fields, str): return field_escape(fields) - else: - return ', '.join(field_escape(fields)) + return ', '.join(field_escape(fields, True)) @staticmethod def _StringGroup(group, field_escape): @@ -86,7 +85,7 @@ def _StringGroup(group, field_escape): return '' elif isinstance(group, str): return 'GROUP BY ' + field_escape(group) - return 'GROUP BY ' + ', '.join(field_escape(group)) + return 'GROUP BY ' + ', '.join(field_escape(group, True)) @staticmethod def _StringLimit(limit, offset): @@ -112,8 +111,7 @@ def _StringOrder(order, field_escape): def _StringTable(table, field_escape): if isinstance(table, str): return field_escape(table) - else: - return ', '.join(field_escape(table)) + return ', '.join(field_escape(table, True)) def Delete(self, table, conditions, order=None, limit=None, offset=0, escape=True): @@ -212,6 +210,9 @@ def Select(self, table, fields=None, conditions=None, order=None, fields: string/list/tuple (optional). Fields to select. Default '*'. As string, single field name. (autoquoted) As list/tuple, one field name per element. (autoquoted) + If the fielname itself is supplied as a tuple, + `field` as `name' will be returned where name is the second + item in the tuple. (autoquoted) conditions: string/list/tuple (optional). SQL 'where' statement. Literal as string. AND'd if list/tuple. THESE WILL NOT BE ESCAPED FOR YOU, EVER. @@ -235,7 +236,7 @@ def Select(self, table, fields=None, conditions=None, order=None, Returns: sqlresult.ResultSet object. """ - field_escape = self.connection.EscapeField if escape else lambda x: x + field_escape = self.connection.EscapeField if escape else self.NoEscapeField result = self._Execute('SELECT %s %s %s FROM %s WHERE %s %s %s %s' % ( 'SQL_CALC_FOUND_ROWS' if totalcount and limit is not None else '', 'DISTINCT' if distinct else '', @@ -249,6 +250,21 @@ def Select(self, table, fields=None, conditions=None, order=None, result.affected = self._Execute('SELECT FOUND_ROWS()')[0][0] return result + def NoEscapeField(self, field, multiple=False): + """Returns a SQL unescaped field or table name. + + Set multiple = True if field is a tuple of names to be returned. + If multiple = False, and a tuple is encountered `field` as `name` will be + returned where the second part of the tuple is the `name` part. + """ + if not field: + return '' + if isinstance(field, str): + return field + elif not multiple and isinstance(field, tuple): + return '%s as %s' % (field[0], field[1]) + return map(self.NoEscapeField, field) + def SelectTables(self, contains=None, exact=False): """Returns table names from the current database. diff --git a/uweb3/model.py b/uweb3/model.py index 56654b4d..c5dc3ead 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -1136,7 +1136,7 @@ def FromPrimary(cls, connection, pkey_value): @classmethod def List(cls, connection, conditions=None, limit=None, offset=None, order=None, yield_unlimited_total_first=False, search=None, - tables=None, escape=True, fields=None): + tables=None, escape=True, fields=None, distinct=False): """Yields a Record object for every table entry. Arguments: @@ -1167,6 +1167,8 @@ def List(cls, connection, conditions=None, limit=None, offset=None, Are conditions escaped? % fields: str / iterable ~~ * Specifies what fields should be returned + % distinct: bool (optional). + Performs a DISTINCT query if set to True. Yields: Record: Database record abstraction class. @@ -1199,7 +1201,7 @@ def List(cls, connection, conditions=None, limit=None, offset=None, table=tables, conditions=conditions, limit=limit, offset=offset, order=order, totalcount=yield_unlimited_total_first, - escape=escape, group=group) + escape=escape, group=group, distinct=distinct) if yield_unlimited_total_first: yield records.affected records = [cls(connection, record) for record in list(records)] From bbf05a6bac50192f29409774253a678843dc5283 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 26 Nov 2020 17:13:56 +0100 Subject: [PATCH 057/118] more and better output for syntax errors in templates --- test/test_templateparser.py | 4 ++-- uweb3/templateparser.py | 42 ++++++++++++++++++++++++++----------- 2 files changed, 32 insertions(+), 14 deletions(-) diff --git a/test/test_templateparser.py b/test/test_templateparser.py index 9e3f19aa..15b541d9 100755 --- a/test/test_templateparser.py +++ b/test/test_templateparser.py @@ -299,7 +299,7 @@ def testNonexistantFuntion(self): template = 'This tag function is missing [num|zoink].' self.assertEqual(self.parse(template), template) # Error is only thrown if we actually pass an argument for the tag: - self.assertRaises(templateparser.TemplateNameError, + self.assertRaises(templateparser.TemplateFunctionError, self.parse, template, num=1) def testAlwaysString(self): @@ -457,7 +457,7 @@ def testVariableClosureArgument(self): should never have access to the python scope.""" test = 20 template = '[tag|limit(test)]' - self.assertRaises(templateparser.TemplateSyntaxError, + self.assertRaises(templateparser.TemplateNameError, self.parse, template, tag=self.tag) def testComplexClosureWithoutArguments(self): diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index bc4a35c0..ee39d232 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -31,7 +31,11 @@ class TemplateKeyError(Error): class TemplateNameError(Error): - """The referenced tag or function does not exist.""" + """The referenced tag does not exist.""" + + +class TemplateFunctionError(Error): + """The referenced function does not exist.""" class TemplateValueError(Error, ValueError): @@ -372,7 +376,7 @@ def AddFile(self, name): raise TypeError('The template requires parser for adding template files.') return self._AddToOpenScope(self.parser[name]) - def AddString(self, raw_template): + def AddString(self, raw_template, filename=None): """Extends the Template by adding a raw template string. The given template is parsed and added to the existing template. @@ -390,8 +394,8 @@ def AddString(self, raw_template): if len(self.scopes) != scope_depth: scope_diff = len(self.scopes) - scope_depth if scope_diff < 0: - raise TemplateSyntaxError('Closed %d scopes too many' % abs(scope_diff)) - raise TemplateSyntaxError('Template left %d open scopes.' % scope_diff) + raise TemplateSyntaxError('Closed %d scopes too many in "%s"' % (abs(scope_diff), filename or raw_template)) + raise TemplateSyntaxError('TemplateString left %d open scopes in "%s"' % (scope_diff, filename or raw_template)) def Parse(self, returnRawTemplate=False, **kwds): """Returns the parsed template as HTMLsafestring. @@ -445,7 +449,9 @@ def _ExtendFunction(self, nodes): try: getattr(self, '_TemplateConstruct%s' % function.title())(*nodes) except AttributeError: - raise TemplateSyntaxError('Unknown template function {{ %s }}' % function) + raise TemplateSyntaxError('Unknown template function {{ %s }}%s' % + (function, + ' in template "%s"' % self._template_path if self._template_path else '')) def _ExtendText(self, node): """Processes a text node and adds its tags and texts to the Template.""" @@ -559,7 +565,10 @@ def Parse(self, **kwds): The template is parsed by parsing each of its members and combining that. """ self.ReloadIfModified() - result = super().Parse(**kwds) + try: + result = super().Parse(**kwds) + except TemplateFunctionError as error: + raise TemplateFunctionError('%s in %s' % (error, self._template_path)) if self.parser and self.parser.noparse: return {'template': self._templatepath[len(self.parser.template_dir):], 'replacements': result.tags, @@ -584,7 +593,7 @@ def ReloadIfModified(self): template = templatefile.read() del self[:] self.scopes = [self] - self.AddString(template) + self.AddString(template, self._file_name) self._file_mtime = mtime except (IOError, OSError): # File cannot be stat'd or read. No longer exists or we lack permissions. @@ -664,7 +673,9 @@ def Expression(self, expr, **kwds): try: return LimitedEval(''.join(nodes), self.astvisitor, local_vars) except NameError as error: - raise TemplateNameError(str(error).capitalize() + '. Try it as tagname?') + raise TemplateNameError(str(error).capitalize() + '. Try it as [tagname]?') + except SyntaxError as error: + raise TemplateSyntaxError('%s while evaluating: %s' % (str(error).capitalize(), ''.join(nodes))) def NewBranch(self, expr): """Begins a new branch based on the given expression.""" @@ -872,6 +883,8 @@ def GetValue(self, replacements): return value except KeyError: raise TemplateNameError('No replacement with name %r' % self.name) + except TemplateKeyError as error: + raise TemplateKeyError('%s on %r' % (error, self.name)) @classmethod def ApplyFunction(cls, func, value): @@ -891,10 +904,10 @@ def ApplyFunction(cls, func, value): raise TemplateTypeError( ('Templatefunction raised an TypeError %s(%s) ' % (func, value), err_obj)) except KeyError as err_obj: - raise TemplateNameError( + raise TemplateFunctionError( 'Unknown template tag function %r' % err_obj.args[0]) except NameError as err_obj: - raise TemplateSyntaxError( + raise TemplateNameError( 'Access to scope outside of parser variables is not allowed: %r' % err_obj.args[0]) def Parse(self, **kwds): @@ -924,7 +937,12 @@ def Parse(self, **kwds): # Process functions, or apply default if value is not Basesafestring if self.functions: for func in self.functions: - value = self.ApplyFunction(func, value) + try: + value = self.ApplyFunction(func, value) + except TemplateFunctionError as error: + raise TemplateFunctionError('%s on %s' % (error, self)) + except TemplateSyntaxError as error: + raise TemplateSyntaxError('%s on %s' % (error, self)) if not isinstance(value, Basesafestring): value = TAG_FUNCTIONS['default'](value) return value @@ -976,7 +994,7 @@ def _GetIndex(haystack, needle): # TypeError: `haystack` is no mapping but may have a matching attr. return getattr(haystack, needle) except (AttributeError, LookupError): - raise TemplateKeyError('Item has no index, key or attribute %r.' % needle) + raise TemplateKeyError('Item has no index, key or attribute %r' % needle) class TemplateText(str): From 338e29a0786948d48cd0c9a8f5cbbfe9d6fa14f0 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 30 Nov 2020 11:45:36 +0100 Subject: [PATCH 058/118] give the hotReload functionality some much needed attention. Now the interval, the ignored extensions, and a new ignored directories list of configurable, of which the later defaults to including the static and template dirs from the pagemaker --- uweb3/__init__.py | 98 +++++++++++++++++++++++++---------------------- 1 file changed, 53 insertions(+), 45 deletions(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 8775a4c7..c385eb30 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -307,26 +307,36 @@ def get_response(self, page_maker, method, args): logger.exception("UNCAUGHT EXCEPTION:") return page_maker.InternalServerError(*sys.exc_info()) - def serve(self, hot_reloading=True): + def serve(self): """Sets up and starts WSGI development server for the current app.""" host = 'localhost' port = 8001 hotreload = False dev = False + interval = None + ignored_directories = ['__pycache__', + self.inital_pagemaker.PUBLIC_DIR, + self.inital_pagemaker.TEMPLATE_DIR] if self.config.options.get('development', False): host = self.config.options['development'].get('host', host) port = self.config.options['development'].get('port', port) - hotreload = self.config.options['development'].get('reload', False) == 'True' - dev = self.config.options['development'].get('dev', False) + hotreload = self.config.options['development'].get('reload', False) in ('True', 'true') + dev = self.config.options['development'].get('dev', False) in ('True', 'true') + interval = int(self.config.options['development'].get('checkinterval', 0)) + ignored_extensions = self.config.options['development'].get('ignored_extensions', '').split(',') + ignored_directories += self.config.options['development'].get('ignored_directories', '').split(',') server = make_server(host, int(port), self) print(f'Running µWeb3 server on http://{server.server_address[0]}:{server.server_address[1]}') print(f'Root dir is: {self.executing_path}') try: if hotreload: print(f'Hot reload is enabled for changes in: {self.executing_path}') - HotReload(self.executing_path, uweb_dev=dev) + HotReload(self.executing_path, interval=interval, dev=dev, + ignored_extensions=ignored_extensions, + ignored_directories=ignored_directories) server.serve_forever() - except: + except Exception as error: + print(error) server.shutdown() def setup_routing(self): @@ -350,63 +360,61 @@ def setup_routing(self): class HotReload(object): """This class handles the thread which scans for file changes in the execution path and restarts the server if needed""" - IGNOREDEXTENSIONS = (".pyc", '.ini', '.md', '.html', '.log') + IGNOREDEXTENSIONS = [".pyc", '.ini', '.md', '.html', '.log', '.sql'] - def __init__(self, path, interval=1, uweb_dev=False): + def __init__(self, path, interval=None, dev=False, ignored_extensions=None, ignored_directories=None): """Takes a path, an optional interval in seconds and an optional flag - signalling a development environment""" + signaling a development environment which will set the path for new and + changed file checking on the parent folder of the serving file.""" import threading - import time - self.running = threading.Event() - self.interval = interval + self.interval = interval or 1 self.path = os.path.dirname(path) - if uweb_dev in ('True', True): + self.ignoredextensions = self.IGNOREDEXTENSIONS + (ignored_extensions or []) + self.ignoreddirectories = ignored_directories + if dev: from pathlib import Path self.path = str(Path(self.path).parents[1]) - self.thread = threading.Thread(target=self.run, args=()) + self.thread = threading.Thread(target=self.Run) self.thread.daemon = True self.thread.start() - def run(self): - """ Method runs forever and watches all files in the project folder. - - Does not trigger a reload when the following files change: - - .pyc - - .ini - - .md - - .html - - .log - - Changes in the HTML are noticed by the TemplateParser, - which then reloads the HTML file into the object and displays the updated version. - """ - self.WATCHED_FILES = self.getListOfFiles()[1] - WATCHED_FILES_MTIMES = [(f, os.path.getmtime(f)) for f in self.WATCHED_FILES] + def Run(self): + """ Method runs forever and watches all files in the project folder.""" + self.watched_files = self.Files() + print(self.watched_files) + self.mtimes = [(f, os.path.getmtime(f)) for f in self.watched_files] + import time while True: - if len(self.WATCHED_FILES) != self.getListOfFiles()[0]: + time.sleep(self.interval) + new = self.Files(self.watched_files) + if new: print('{color}New file added or deleted\x1b[0m \nRestarting µWeb3'.format(color='\x1b[7;30;41m')) - self.restart() - for f, mtime in WATCHED_FILES_MTIMES: + self.Restart() + for f, mtime in self.mtimes: if os.path.getmtime(f) != mtime: print('{color}Detected changes in {file}\x1b[0m \nRestarting µWeb3'.format(color='\x1b[7;30;41m', file=f)) - self.restart() - time.sleep(self.interval) - - def getListOfFiles(self): - """Returns all files inside the working directory of uweb3. - Also returns a count so that we can restart on file add/remove. - """ - watched_files = [] - for r, d, f in os.walk(self.path): - for file in f: + self.Restart() + + def Files(self, current=None): + """Returns all files inside the working directory of uweb3.""" + if not current: + current = set() + new = set() + for dirpath, dirnames, filenames in os.walk(self.path): + if any(list(map(lambda dirname: dirname in dirpath, self.ignoreddirectories))): + continue + for file in filenames: + fullname = os.path.join(dirpath, file) + if fullname in current or fullname.endswith('~'): + continue ext = os.path.splitext(file)[1] - if ext not in IGNOREDEXTENSIONS: - watched_files.append(os.path.join(r, file)) - return (len(watched_files), watched_files) + if ext not in self.ignoredextensions: + new.add(fullname) + return new - def restart(self): + def Restart(self): """Restart uweb3 with all provided system arguments.""" self.running.clear() os.execl(sys.executable, sys.executable, * sys.argv) From 9faeffb37816b8deff09e9a0b97b683c575f98c3 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 30 Nov 2020 11:48:13 +0100 Subject: [PATCH 059/118] remove unneeded print output in reload module --- uweb3/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index c385eb30..0ce8fb95 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -382,7 +382,6 @@ def __init__(self, path, interval=None, dev=False, ignored_extensions=None, igno def Run(self): """ Method runs forever and watches all files in the project folder.""" self.watched_files = self.Files() - print(self.watched_files) self.mtimes = [(f, os.path.getmtime(f)) for f in self.watched_files] import time From f794a7a6f45478c5e6e6752cde1f82850a467f92 Mon Sep 17 00:00:00 2001 From: Erwin Hager Date: Tue, 8 Dec 2020 15:58:33 +0100 Subject: [PATCH 060/118] import parse_qs from urllib instead of cgi with python3.8 --- uweb3/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uweb3/request.py b/uweb3/request.py index 70c80201..098a39c5 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -7,7 +7,7 @@ import sys import urllib import io -from cgi import parse_qs +from urllib.parse import parse_qs import io as stringIO import http.cookies as cookie import re From 63aaf6e3657f445f3bb82f733aa0005fd1f35a7b Mon Sep 17 00:00:00 2001 From: Erwin Hager Date: Tue, 8 Dec 2020 16:02:48 +0100 Subject: [PATCH 061/118] change xrange to range in tests --- uweb3/libs/sqltalk/sqlresult_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uweb3/libs/sqltalk/sqlresult_test.py b/uweb3/libs/sqltalk/sqlresult_test.py index ea131263..d0a43b76 100644 --- a/uweb3/libs/sqltalk/sqlresult_test.py +++ b/uweb3/libs/sqltalk/sqlresult_test.py @@ -186,8 +186,8 @@ class ResultSetBasicOperation(unittest.TestCase): def setUp(self): """Set up a persistent test environment.""" self.fields = ('first', 'second', 'third', 'fourth') - self.result = tuple(tuple(2 ** i for i in xrange(j, j + 4)) - for j in xrange(0, 13, 4)) + self.result = tuple(tuple(2 ** i for i in range(j, j + 4)) + for j in range(0, 13, 4)) def testFalseWhenEmpty(self): """ResultSet is boolean False when there's all but a result.""" From 8f00ed4d8d1df6a6d44ef702e79fa73575eb7c85 Mon Sep 17 00:00:00 2001 From: Erwin Hager Date: Tue, 8 Dec 2020 16:09:42 +0100 Subject: [PATCH 062/118] relative import in sqlresult test --- uweb3/libs/sqltalk/sqlresult_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uweb3/libs/sqltalk/sqlresult_test.py b/uweb3/libs/sqltalk/sqlresult_test.py index d0a43b76..c584a771 100644 --- a/uweb3/libs/sqltalk/sqlresult_test.py +++ b/uweb3/libs/sqltalk/sqlresult_test.py @@ -22,7 +22,7 @@ import unittest # Unittest target -import sqlresult +from . import sqlresult class ResultRowBasicOperation(unittest.TestCase): From 9dff33229ea828ae7dc1a3e86bfdb53fc8c75ba8 Mon Sep 17 00:00:00 2001 From: Erwin Hager Date: Tue, 8 Dec 2020 16:09:56 +0100 Subject: [PATCH 063/118] update gitignore to ignore .noseids --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 22280549..225064a7 100644 --- a/.gitignore +++ b/.gitignore @@ -25,6 +25,7 @@ pip-log.txt .coverage .tox nosetests.xml +.noseids # Translations *.mo From 18e52bb6205a268763e5ab9ff6303b91795a03ef Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 9 Dec 2020 17:24:10 +0100 Subject: [PATCH 064/118] add some methods for header handling, and copy the already present headers on the request to any newly created Response objects. This allows users to set outbound headers in the PostInit by using the self.req.AddHeader method already present there. --- uweb3/request.py | 8 +++++--- uweb3/response.py | 10 +++++++++- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/uweb3/request.py b/uweb3/request.py index 70c80201..14914011 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -83,13 +83,13 @@ def path(self): @property def response(self): if self._response is None: - self._response = response.Response() + self._response = response.Response(headers=self._out_headers) return self._response def Redirect(self, location, httpcode=307): REDIRECT_PAGE = ('Page moved' - 'Page moved, please follow this link' - '').format(location) + 'Page moved, please follow this link' + '').format(location) headers = {'Location': location} if self.response.headers.get('Set-Cookie'): @@ -152,6 +152,8 @@ def AddHeader(self, name, value): self.response.headers['Set-Cookie'] = [value] return self.response.headers['Set-Cookie'].append(value) + return + self.response.AddHeader(name, value) def DeleteCookie(self, name): """Deletes cookie by name diff --git a/uweb3/response.py b/uweb3/response.py index 816a4593..b1cbbd19 100644 --- a/uweb3/response.py +++ b/uweb3/response.py @@ -22,7 +22,7 @@ class Response(object): def __init__(self, content='', content_type=CONTENT_TYPE, httpcode=200, headers=None, **kwds): - """Initializes a Page object. + """Initializes a Response object. Arguments: @ content: str @@ -97,6 +97,14 @@ def __repr__(self): def __str__(self): return self.content + def SetHeaders(self, headers): + """Instantly set all headers for this Response """ + self.headers = headers + + def AddHeader(self, header, value): + """Adds a header to this response's output list""" + self.headers[header] = value + class Redirect(Response): """A response tailored to do redirects.""" From 8ee2936e1b51dbb5453d402d751245dacdc2e522 Mon Sep 17 00:00:00 2001 From: Erwin Hager Date: Fri, 11 Dec 2020 13:48:27 +0100 Subject: [PATCH 065/118] don't use const naming style for mutable properties --- uweb3/model.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/uweb3/model.py b/uweb3/model.py index c5dc3ead..67da0076 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -50,17 +50,17 @@ def __init__(self, filename=None, path=None): extension = '' if filename and filename.endswith(('.ini', '.conf')) else '.ini' if filename: - self.FILENAME = f"{filename[:1].lower() + filename[1:] + extension}" + self.filename = f"{filename[:1].lower() + filename[1:] + extension}" else: - self.FILENAME = self.TableName() + extension + self.filename = self.TableName() + extension if path and not filename.startswith('/'): - self.FILE_LOCATION = os.path.join(path, self.FILENAME) + self.file_location = os.path.join(path, self.filename) else: - self.FILE_LOCATION = self.FILENAME + self.file_location = self.filename self.__CheckPermissions() - if not os.path.isfile(self.FILE_LOCATION): - os.mknod(self.FILE_LOCATION) + if not os.path.isfile(self.file_location): + os.mknod(self.file_location) self.mtime = None self.config = configparser.ConfigParser() @@ -82,12 +82,12 @@ def TableName(cls): def __CheckPermissions(self): """Checks if SettingsManager can read/write to file.""" - if not os.path.isfile(self.FILE_LOCATION): + if not os.path.isfile(self.file_location): return True - if not os.access(self.FILE_LOCATION, os.R_OK): - raise PermissionError(f"SettingsManager missing permissions to read file: {self.FILE_LOCATION}") - if not os.access(self.FILE_LOCATION, os.W_OK): - raise PermissionError(f"SettingsManager missing permissions to write to file: {self.FILE_LOCATION}") + if not os.access(self.file_location, os.R_OK): + raise PermissionError(f"SettingsManager missing permissions to read file: {self.file_location}") + if not os.access(self.file_location, os.W_OK): + raise PermissionError(f"SettingsManager missing permissions to write to file: {self.file_location}") def Create(self, section, key, value): """Creates a section or/and key = value @@ -116,10 +116,10 @@ def Read(self): """Reads the config file and populates the options member It uses the mtime to see if any re-reading is required""" if not self.mtime: - curtime = os.path.getmtime(self.FILE_LOCATION) + curtime = os.path.getmtime(self.file_location) if self.mtime and self.mtime == curtime: return False - self.config.read(self.FILE_LOCATION) + self.config.read(self.file_location) self.options = self.config._sections self.mtime = curtime return True @@ -166,7 +166,7 @@ def Delete(self, section, key=None): def _Write(self, reread=True): """Internal function to store the current config to file""" - with open(self.FILE_LOCATION, 'w') as configfile: + with open(self.file_location, 'w') as configfile: self.config.write(configfile) if reread: return self.Read() From 90905f620de2feab12f4dcbb00a9c0765874e5bd Mon Sep 17 00:00:00 2001 From: Erwin Hager Date: Fri, 11 Dec 2020 14:03:57 +0100 Subject: [PATCH 066/118] python3 and comprehensions --- uweb3/model.py | 37 ++++++++++++++++--------------------- uweb3/request.py | 13 ++++++------- uweb3/templateparser.py | 10 +++++----- 3 files changed, 27 insertions(+), 33 deletions(-) diff --git a/uweb3/model.py b/uweb3/model.py index 67da0076..8d7e9221 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -663,20 +663,16 @@ def _LoadAsForeign(cls, connection, relation_value, method=None): def _Changes(self): """Returns the differences of the current state vs the last stored state.""" sql_record = self._DataRecord() - changes = {} - for key, value in sql_record.items(): - if self._record.get(key) != value: - changes[key] = value - return changes + return { + key: value + for key, value in sql_record.items() if self._record.get(key) != value + } def _DataRecord(self): """Returns a dictionary of the record's database values For any Record object present, its primary key value (`Record.key`) is used. """ - sql_record = {} - for key, value in super().items(): - sql_record[key] = self._ValueOrPrimary(value) - return sql_record + return {key: self._ValueOrPrimary(value) for key, value in super().items()} @staticmethod def _ValueOrPrimary(value): @@ -1205,8 +1201,9 @@ def List(cls, connection, conditions=None, limit=None, offset=None, if yield_unlimited_total_first: yield records.affected records = [cls(connection, record) for record in list(records)] - for record in records: - yield record + + yield from records + if cacheable: list(cls._cacheListPreseed(records)) @@ -1382,14 +1379,14 @@ def List(cls, connection, conditions=None, limit=None, offset=None, """ if not tables: tables = [cls.TableName()] - if not fields: - fields = "%s.*" % cls.TableName() - else: + if fields: if fields != '*': - if type(fields) != str: - fields = ', '.join(connection.EscapeField(fields)) - else: + if isinstance(fields, str): fields = connection.EscapeField(fields) + else: + fields = ', '.join(connection.EscapeField(fields)) + else: + fields = "%s.*" % cls.TableName() if search: search = search.strip() tables, searchconditions = cls._GetSearchQuery(connection, tables, search) @@ -1438,8 +1435,7 @@ def List(cls, connection, conditions=None, limit=None, offset=None, yield records.affected # turn sqltalk rows into model records = [cls(connection, record) for record in list(records)] - for record in records: - yield record + yield from records if cacheable: list(cls._cacheListPreseed(records)) @@ -1604,8 +1600,7 @@ def GetSubTypes(cls, seen=None): if sub not in seen: seen.add(sub) yield sub - for sub in GetSubTypes(sub, seen): - yield sub + yield from GetSubTypes(sub, seen) for cls in GetSubTypes(BaseRecord): # Do not yield subclasses defined in this module diff --git a/uweb3/request.py b/uweb3/request.py index 098a39c5..70343337 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -136,9 +136,8 @@ def AddCookie(self, key, value, **attrs): When True, the cookie is only used for http(s) requests, and is not accessible through Javascript (DOM). """ - if isinstance(value, (str)): - if len(value.encode('utf-8')) >= 4096: - raise CookieTooBigError("Cookie is larger than 4096 bytes and wont be set") + if isinstance(value, (str)) and len(value.encode('utf-8')) >= 4096: + raise CookieTooBigError("Cookie is larger than 4096 bytes and wont be set") new_cookie = Cookie({key: value}) if 'max_age' in attrs: @@ -202,10 +201,10 @@ def __repr__(self): @property def __dict__(self): - d = {} - for key, value in self.iteritems(): - d[key] = value if len(value) > 1 else value[0] - return d + return { + key: value if len(value) > 1 else value[0] + for key, value in self.iteritems() + } class QueryArgsDict(dict): diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index ee39d232..242510fc 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -274,10 +274,7 @@ def RegisterTag(self, tag, value, persistent=False): @ persistent: bool will this tag be present for multiple requests? """ - if persistent: - storage = self.tags - else: - storage = self.requesttags + storage = self.tags if persistent else self.requesttags if ':' not in tag: storage[tag] = value return @@ -826,7 +823,10 @@ def __init__(self, name, indices=(), functions=()): Names of template functions that should be applied to the value. """ self.name = name - self.indices = indices if self.ALLOWPRIVATE else list(index for index in indices if not index.startswith('_') or not index.endswith('_')) + self.indices = (indices if self.ALLOWPRIVATE else [ + index for index in indices + if not index.startswith('_') or not index.endswith('_') + ]) self.functions = functions def __repr__(self): From 715ca4ac085cb2e90295bfa15212969448f671b7 Mon Sep 17 00:00:00 2001 From: Erwin Hager Date: Fri, 11 Dec 2020 14:09:48 +0100 Subject: [PATCH 067/118] use parse_qs and parse_qsl from urllib.parse instead of cgi --- uweb3/request.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/uweb3/request.py b/uweb3/request.py index 098a39c5..7c9e651a 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -7,7 +7,7 @@ import sys import urllib import io -from urllib.parse import parse_qs +from urllib.parse import parse_qs, parse_qsl import io as stringIO import http.cookies as cookie import re @@ -60,7 +60,7 @@ def __init__(self, env, registry): self.method = self.env['REQUEST_METHOD'] self.vars = {'cookie': dict((name, value.value) for name, value in Cookie(self.env.get('HTTP_COOKIE')).items()), - 'get': QueryArgsDict(cgi.parse_qs(self.env['QUERY_STRING']))} + 'get': QueryArgsDict(parse_qs(self.env['QUERY_STRING']))} self.env['host'] = self.headers.get('Host', '') if self.method in ('POST', 'PUT', 'DELETE'): request_body_size = 0 @@ -185,7 +185,7 @@ def items(self): def read_urlencoded(self): indexed = {} self.list = [] - for field, value in cgi.parse_qsl(self.fp.read(self.length), + for field, value in parse_qsl(self.fp.read(self.length), self.keep_blank_values, self.strict_parsing): if self.FIELD_AS_ARRAY.match(str(field)): From 5fec7f9a130d89b9c335d6696907f66860aefbc7 Mon Sep 17 00:00:00 2001 From: Erwin Hager Date: Fri, 11 Dec 2020 14:22:52 +0100 Subject: [PATCH 068/118] remove (object) from classes and more comprehensions and generators --- test/test_templateparser.py | 6 ++-- uweb3/__init__.py | 39 ++++++++++++------------- uweb3/alchemy_model.py | 36 +++++++++++++---------- uweb3/connections.py | 31 ++++++++++---------- uweb3/libs/mail.py | 4 +-- uweb3/libs/sqltalk/sqlite/connection.py | 2 +- uweb3/libs/sqltalk/sqlite/cursor.py | 7 +++-- uweb3/libs/sqltalk/sqlresult.py | 4 +-- uweb3/libs/utils.py | 23 ++++++++------- uweb3/model.py | 6 ++-- uweb3/pagemaker/__init__.py | 24 +++++++-------- uweb3/request.py | 12 +++++--- uweb3/response.py | 2 +- uweb3/sockets.py | 2 +- uweb3/templateparser.py | 8 ++--- 15 files changed, 107 insertions(+), 99 deletions(-) diff --git a/test/test_templateparser.py b/test/test_templateparser.py index 15b541d9..4f67a13b 100755 --- a/test/test_templateparser.py +++ b/test/test_templateparser.py @@ -77,8 +77,8 @@ class ParserPerformance(unittest.TestCase): @staticmethod def testPerformance(): """[Parser] Basic performance test for 2 template replacements""" + template = 'This [obj:foo] is just a quick [bar]' for _template in range(100): - template = 'This [obj:foo] is just a quick [bar]' tmpl = templateparser.Template(template) for _parse in range(100): tmpl.Parse(obj={'foo': 'template'}, bar='hack') @@ -148,7 +148,7 @@ def testBadCharacterTags(self): bad_chars = """ :~!@#$%^&*()+-={}\|;':",./<>? """ template = ''.join('[%s] [check]' % char for char in bad_chars) expected = ''.join('[%s] ..' % char for char in bad_chars) - replaces = dict((char, 'FAIL') for char in bad_chars) + replaces = {char: 'FAIL' for char in bad_chars} replaces['check'] = '..' self.assertEqual(self.tmpl(template).Parse(**replaces), expected) @@ -253,7 +253,7 @@ def testTemplateUnderscoreCharacters(self): def testTemplateMissingIndexes(self): """[IndexedTag] Tags with bad indexes will be returned verbatim""" - class Object(object): + class Object: """A simple object to store an attribute on.""" NAME = 'Freeman' diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 0ce8fb95..f10f8e3b 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -37,11 +37,11 @@ class HTTPRequestException(HTTPException): class NoRouteError(Error): """The server does not know how to route this request""" -class Registry(object): +class Registry: """Something to hook stuff to""" -class Router(object): +class Router: def __init__(self, page_class): self.pagemakers = page_class.LoadModules() self.pagemakers.append(page_class) @@ -119,9 +119,8 @@ def request_router(url, method, host): for pattern, handler, routemethod, hostpattern, page_maker in req_routes: if routemethod != 'ALL': # clearly not the route we where looking for - if isinstance(routemethod, tuple): - if method not in routemethod: - continue + if isinstance(routemethod, tuple) and method not in routemethod: + continue if method != routemethod: continue @@ -143,7 +142,7 @@ def request_router(url, method, host): return request_router -class uWeb(object): +class uWeb: """Returns a configured closure for handling page requests. This closure is configured with a precomputed set of routes and handlers using @@ -167,7 +166,7 @@ class uWeb(object): RequestHandler: Configured closure that is ready to process requests. """ def __init__(self, page_class, routes, executing_path=None, config='config'): - self.executing_path = executing_path if executing_path else os.path.dirname(__file__) + self.executing_path = executing_path or os.path.dirname(__file__) self.config = SettingsManager(filename=config, path=self.executing_path) self.logger = self.setup_logger() self.inital_pagemaker = page_class @@ -269,9 +268,10 @@ def _logging(self, req, response): """Logs incoming requests to a logfile. This is enabled by default, even if its missing in the config file. """ - if self.config.options.get('development', None): - if self.config.options['development'].get('access_logging', True) == 'False': - return + if (self.config.options.get('development', None) + and self.config.options['development'].get('access_logging', + True) == 'False'): + return host = req.env['HTTP_HOST'].split(':')[0] date = datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S') @@ -299,12 +299,13 @@ def get_response(self, page_maker, method, args): except ImmediateResponse as err: return err[0] except Exception: - if self.config.options.get('development', False): - if self.config.options['development'].get('error_logging', True) == 'True': - logger = logging.getLogger('uweb3_exception_logger') - fh = logging.FileHandler(os.path.join(self.executing_path, 'uweb3_uncaught_exceptions.log')) - logger.addHandler(fh) - logger.exception("UNCAUGHT EXCEPTION:") + if (self.config.options.get('development', False) + and self.config.options['development'].get('error_logging', + True) == 'True'): + logger = logging.getLogger('uweb3_exception_logger') + fh = logging.FileHandler(os.path.join(self.executing_path, 'uweb3_uncaught_exceptions.log')) + logger.addHandler(fh) + logger.exception("UNCAUGHT EXCEPTION:") return page_maker.InternalServerError(*sys.exc_info()) def serve(self): @@ -341,9 +342,7 @@ def serve(self): def setup_routing(self): if isinstance(self.inital_pagemaker, list): - routes = [] - for route in self.inital_pagemaker[1:]: - routes.append(route) + routes = [route for route in self.inital_pagemaker[1:]] self.inital_pagemaker[0].AddRoutes(tuple(routes)) self.inital_pagemaker = self.inital_pagemaker[0] @@ -357,7 +356,7 @@ def setup_routing(self): self.inital_pagemaker.LoadModules(routes=default_route) -class HotReload(object): +class HotReload: """This class handles the thread which scans for file changes in the execution path and restarts the server if needed""" IGNOREDEXTENSIONS = [".pyc", '.ini', '.md', '.html', '.log', '.sql'] diff --git a/uweb3/alchemy_model.py b/uweb3/alchemy_model.py index 19b44a0b..f9536ad7 100644 --- a/uweb3/alchemy_model.py +++ b/uweb3/alchemy_model.py @@ -9,25 +9,26 @@ from uweb3.model import NotExistError -class AlchemyBaseRecord(object): +class AlchemyBaseRecord: def __init__(self, session, record): self.session = session self._BuildClassFromRecord(record) def _BuildClassFromRecord(self, record): - if isinstance(record, dict): - for key, value in record.items(): - if not key in self.__table__.columns.keys(): - raise AttributeError(f"Key '{key}' not specified in class '{self.__class__.__name__}'") - setattr(self, key, value) - if self.session: - try: - self.session.add(self) - except: - self.session.rollback() - raise - else: - self.session.commit() + if not isinstance(record, dict): + return + for key, value in record.items(): + if key not in self.__table__.columns.keys(): + raise AttributeError(f"Key '{key}' not specified in class '{self.__class__.__name__}'") + setattr(self, key, value) + if self.session: + try: + self.session.add(self) + except: + self.session.rollback() + raise + else: + self.session.commit() def __hash__(self): """Returns the hashed value of the key.""" @@ -71,7 +72,10 @@ def __ne__(self, other): return not self == other def __len__(self): - return len(dict((col, getattr(self, col)) for col in self.__table__.columns.keys() if getattr(self, col))) + return len({ + col: getattr(self, col) + for col in self.__table__.columns.keys() if getattr(self, col) + }) def __int__(self): """Returns the integer key value of the Record. @@ -180,7 +184,7 @@ def _AlchemyRecordToDict(cls, record): None: when record is empty """ if not isinstance(record, type(None)): - return dict((col, getattr(record, col)) for col in record.__table__.columns.keys()) + return {col: getattr(record, col) for col in record.__table__.columns.keys()} return None @reconstructor diff --git a/uweb3/connections.py b/uweb3/connections.py index ee9bfc55..c28dcb43 100644 --- a/uweb3/connections.py +++ b/uweb3/connections.py @@ -12,7 +12,7 @@ class ConnectionError(Exception): """Error class thrown when the underlying connectors thrown an error on connecting.""" -class ConnectionManager(object): +class ConnectionManager: """This is the connection manager object that is handled by all Model Objects. It finds out which connection was requested by looking at the call stack, and figuring out what database type the model class calling it belongs to. @@ -70,15 +70,14 @@ def RelevantConnection(self, level=2): if (con_type in self.__connections and hasattr(self.__connections[con_type], 'connection')): return self.__connections[con_type].connection - else: - request = sys._getframe(3).f_locals['self'].req - try: - # instantiate a connection - self.__connections[con_type] = self.__connectors[con_type]( - self.config, self.options, request, self.debug) - return self.__connections[con_type].connection - except KeyError as error: - raise TypeError('No connector for: %r, available: %r, %r' % (con_type, self.__connectors, error)) + request = sys._getframe(3).f_locals['self'].req + try: + # instantiate a connection + self.__connections[con_type] = self.__connectors[con_type]( + self.config, self.options, request, self.debug) + return self.__connections[con_type].connection + except KeyError as error: + raise TypeError('No connector for: %r, available: %r, %r' % (con_type, self.__connectors, error)) def __enter__(self): """Proxies the transaction to the underlying relevant connection.""" @@ -103,11 +102,11 @@ def RollbackAll(self): def PostRequest(self): """This cleans up any non persistent connections.""" - cleanups = [] - for classname in self.__connections: - if (hasattr(self.__connections[classname], 'PERSISTENT') and - not self.__connections[classname].PERSISTENT): - cleanups.append(classname) + cleanups = [ + classname for classname in self.__connections + if (hasattr(self.__connections[classname], 'PERSISTENT') + and not self.__connections[classname].PERSISTENT) + ] for classname in cleanups: try: self.__connections[classname].Disconnect() @@ -133,7 +132,7 @@ def __del__(self): pass -class Connector(object): +class Connector: """Base Connector class, subclass from this to create your own connectors. Usually the name of your class is used to lookup its config in the configuration file, or the database or local filename. diff --git a/uweb3/libs/mail.py b/uweb3/libs/mail.py index 22a1c8cd..60e6ad10 100644 --- a/uweb3/libs/mail.py +++ b/uweb3/libs/mail.py @@ -18,7 +18,7 @@ class MailError(Exception): """Something went wrong sending your email""" -class MailSender(object): +class MailSender: """Easy context-interface for sending mail.""" def __init__(self, host='localhost', port=25, local_hostname=None, timeout=5): @@ -52,7 +52,7 @@ def __exit__(self, *_exc_args): self.server.quit() -class SendMailContext(object): +class SendMailContext: """Context to use for sending emails.""" def __init__(self, server): """Stores the server object locally.""" diff --git a/uweb3/libs/sqltalk/sqlite/connection.py b/uweb3/libs/sqltalk/sqlite/connection.py index e9ff1191..94598a48 100644 --- a/uweb3/libs/sqltalk/sqlite/connection.py +++ b/uweb3/libs/sqltalk/sqlite/connection.py @@ -164,7 +164,7 @@ def ShowTables(self): return [row[0] for row in result] -class SqliteResult(object): +class SqliteResult: def __init__(self, result, description, rowcount, lastrowid): self.result = result self.description = description diff --git a/uweb3/libs/sqltalk/sqlite/cursor.py b/uweb3/libs/sqltalk/sqlite/cursor.py index 54e63898..4b72b4aa 100644 --- a/uweb3/libs/sqltalk/sqlite/cursor.py +++ b/uweb3/libs/sqltalk/sqlite/cursor.py @@ -8,7 +8,7 @@ from .. import sqlresult -class Cursor(object): +class Cursor: def __init__(self, connection): self.connection = connection self.cursor = connection.cursor() @@ -22,14 +22,15 @@ def Execute(self, query, args=(), many=False): except Exception: self.connection.logger.exception('Exception during query execution') raise - fieldnames = list(field[0] for field in result.description) + fieldnames = [field[0] for field in result.description] return sqlresult.ResultSet( affected=result.rowcount, charset='utf-8', fields=fieldnames, insertid=result.lastrowid, query=(query, tuple(args)), - result=list(dict(zip(fieldnames, row)) for row in result.fetchall())) + result=[dict(zip(fieldnames, row)) for row in result.fetchall()], + ) def Insert(self, table, values): if not values: diff --git a/uweb3/libs/sqltalk/sqlresult.py b/uweb3/libs/sqltalk/sqlresult.py index 968ba700..a4913d78 100644 --- a/uweb3/libs/sqltalk/sqlresult.py +++ b/uweb3/libs/sqltalk/sqlresult.py @@ -32,7 +32,7 @@ class NotSupportedError(Error, TypeError): """Operation is not supported.""" -class ResultRow(object): +class ResultRow: """SQL Result row - an ordered dictionary-like record abstraction. ResultRow has two item retrieval interfaces: @@ -168,7 +168,7 @@ def popitem(self): return self._fields.pop(), self._values.pop() -class ResultSet(object): +class ResultSet: """SQL Result set - stores the query, the returned result, and other info. ResultSet is created from immutable objects. Once defined, none of its diff --git a/uweb3/libs/utils.py b/uweb3/libs/utils.py index 477164e3..b4aaf570 100644 --- a/uweb3/libs/utils.py +++ b/uweb3/libs/utils.py @@ -56,7 +56,7 @@ class cached_property(property): and then that calculated result is used the next time you access the value:: - class Foo(object): + class Foo: @cached_property def foo(self): @@ -97,7 +97,7 @@ class environ_property(_DictAccessorProperty): for the Werzeug request object, but also any other class with an environ attribute: - >>> class Test(object): + >>> class Test: ... environ = {'key': 'value'} ... test = environ_property('key') >>> var = Test() @@ -126,7 +126,7 @@ def lookup(self, obj): return obj.headers -class HTMLBuilder(object): +class HTMLBuilder: """Helper object for HTML generation. Per default there are two instances of that class. The `html` one, and @@ -228,7 +228,7 @@ def proxy(*children, **arguments): buffer += ">" children_as_string = "".join( - [text_type(x) for x in children if x is not None] + text_type(x) for x in children if x is not None ) if children_as_string: @@ -590,8 +590,7 @@ def find_modules(import_path, include_packages=False, recursive=False): if include_packages: yield modname if recursive: - for item in find_modules(modname, include_packages, True): - yield item + yield from find_modules(modname, include_packages, True) else: yield modname @@ -671,15 +670,17 @@ def bind_arguments(func, args, kwargs): vararg_var, kwarg_var, ) = _parse_signature(func)(args, kwargs) - values = {} - for (name, _has_default, _default), value in zip(arg_spec, args): - values[name] = value + values = { + name: value + for (name, _has_default, _default), value in zip(arg_spec, args) + } + if vararg_var is not None: values[vararg_var] = tuple(extra_positional) elif extra_positional: raise TypeError("too many positional arguments") if kwarg_var is not None: - multikw = set(extra) & set([x[0] for x in arg_spec]) + multikw = set(extra) & {x[0] for x in arg_spec} if multikw: raise TypeError( "got multiple values for keyword argument " + repr(next(iter(multikw))) @@ -738,7 +739,7 @@ def __init__(self, import_name, exception): else: track = ["- %r found in %r." % (n, i) for n, i in tracked] track.append("- %r not found." % name) - msg = msg % ( + msg %= ( import_name, "\n".join(track), exception.__class__.__name__, diff --git a/uweb3/model.py b/uweb3/model.py index 8d7e9221..3f44f3c5 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -35,7 +35,7 @@ class PermissionError(Error): """The entity has insufficient rights to access the resource.""" -class SettingsManager(object): +class SettingsManager: def __init__(self, filename=None, path=None): """Creates a ini file with the child class name @@ -173,7 +173,7 @@ def _Write(self, reread=True): return True -class SecureCookie(object): +class SecureCookie: """The secureCookie class works just like other data abstraction classes, except that it stores its data in client side cookies that are signed with a server side secret to avoid tampering by the end-user. @@ -1610,7 +1610,7 @@ def GetSubTypes(cls, seen=None): import functools -class CachedPage(object): +class CachedPage: """Abstraction class for the cached Pages table in the database.""" MAXAGE = 61 diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 3bbeae2e..f134fba3 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -25,7 +25,7 @@ class ReloadModules(Exception): """Signals the handler that it should reload the pageclass""" -class CacheStorage(object): +class CacheStorage: """A (semi) persistent storage for the PageMaker.""" def __init__(self): super(CacheStorage, self).__init__() @@ -143,7 +143,7 @@ def update(self, data=None, **kwargs): self.update(kwargs) -class XSRFToken(object): +class XSRFToken: def __init__(self, seed, remote_addr): self.seed = seed self.remote_addr = remote_addr @@ -161,7 +161,7 @@ def generate_token(self): return h.hexdigest() -class Base(object): +class Base: # Constant for persistent storage accross requests. This will be accessible # by all threads of the same application (in the same Python process). PERSISTENT = CacheStorage() @@ -209,7 +209,7 @@ def Connect(self, sid, env): self.req = env -class XSRFMixin(object): +class XSRFMixin: """Provides XSRF protection by enabling setting xsrf token cookies, checking them and setting a flag based on their value @@ -262,7 +262,7 @@ def _Get_XSRF(self): return self._Set_XSRF_cookie() -class LoginMixin(object): +class LoginMixin: """This mixin provides a few methods that help with handling logins, sessions and related database/cookie interaction""" @@ -336,10 +336,9 @@ def LoadModules(cls, routes='routes/*.py'): module = os.path.relpath(os.path.join(os.getcwd(), file[:-3])).replace('/', '.') classlist = pyclbr.readmodule_ex(module) for name, data in classlist.items(): - if hasattr(data, 'super'): - if 'PageMaker' in data.super[0]: - module = __import__(f, fromlist=[name]) - bases.append(getattr(module, name)) + if hasattr(data, 'super') and 'PageMaker' in data.super[0]: + module = __import__(f, fromlist=[name]) + bases.append(getattr(module, name)) return bases def _PostInit(self): @@ -450,7 +449,7 @@ def _PostRequest(self): self.connection.PostRequest() -class DebuggerMixin(object): +class DebuggerMixin: """Replaces the default handler for Internal Server Errors. This one prints a host of debugging and request information, though it still @@ -537,7 +536,7 @@ def InternalServerError(self, exc_type, exc_value, traceback): error_template.Parse(**exception_data), httpcode=500) -class CSPMixin(object): +class CSPMixin: """Provides CSP header output. https://content-security-policy.com/ @@ -596,7 +595,8 @@ def _CSPFromConfig(self, config): def _CSPheaders(self): """Adds the constructed CSP header to the request""" - csp = '; '.join(["%s %s" % (key, ' '.join(value)) for key, value in self._csp.items()]) + csp = '; '.join( + "%s %s" % (key, ' '.join(value)) for key, value in self._csp.items()) self.req.AddHeader('Content-Security-Policy', csp) # ############################################################################## diff --git a/uweb3/request.py b/uweb3/request.py index 70343337..06091d21 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -49,7 +49,7 @@ def _BaseCookie__set(self, key, real_value, coded_value): dict.__setitem__(self, key, morsel) -class Request(object): +class Request: def __init__(self, env, registry): self.env = env self.headers = dict(self.headers_from_env(env)) @@ -58,9 +58,13 @@ def __init__(self, env, registry): self._out_status = 200 self._response = None self.method = self.env['REQUEST_METHOD'] - self.vars = {'cookie': dict((name, value.value) for name, value in - Cookie(self.env.get('HTTP_COOKIE')).items()), - 'get': QueryArgsDict(cgi.parse_qs(self.env['QUERY_STRING']))} + self.vars = { + 'cookie': { + name: value.value + for name, value in Cookie(self.env.get('HTTP_COOKIE')).items() + }, + 'get': QueryArgsDict(cgi.parse_qs(self.env['QUERY_STRING'])), + } self.env['host'] = self.headers.get('Host', '') if self.method in ('POST', 'PUT', 'DELETE'): request_body_size = 0 diff --git a/uweb3/response.py b/uweb3/response.py index 816a4593..20080df3 100644 --- a/uweb3/response.py +++ b/uweb3/response.py @@ -11,7 +11,7 @@ from collections import defaultdict -class Response(object): +class Response: """Defines a full HTTP response. The full response consists of a required content part, and then optional diff --git a/uweb3/sockets.py b/uweb3/sockets.py index 68624e39..d6cc218c 100644 --- a/uweb3/sockets.py +++ b/uweb3/sockets.py @@ -15,7 +15,7 @@ def __init__(self, socketio_server, uweb3_server, socketio_path='socket.io'): socketio_path=socketio_path ) -class Uweb3SocketIO(object): +class Uweb3SocketIO: def __init__(self, app, sio, static_dir=os.path.dirname(os.path.abspath(__file__))): if not isinstance(app, uWeb): raise Exception("App must be an uWeb3 instance!") diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index 242510fc..aa2e6364 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -58,7 +58,7 @@ class TemplateEvaluationError(Error): """Template condition was not within allowed set of operators.""" -class LazyTagValueRetrieval(object): +class LazyTagValueRetrieval: """Provides a means for lazy tag value retrieval. This is necessary for instance for TemplateConditional.Expression, where @@ -598,7 +598,7 @@ def ReloadIfModified(self): pass -class TemplateConditional(object): +class TemplateConditional: """A template construct to control flow based on the value of a tag.""" def __init__(self, expr, astvisitor): self.branches = [] @@ -790,7 +790,7 @@ def Parse(self, **kwds): return ''.join(output) -class TemplateTag(object): +class TemplateTag: """Template tags are used for dynamic placeholders in templates. Their final value is determined during parsing. For more explanation on this, @@ -1011,7 +1011,7 @@ def Parse(self, **_kwds): return str(self) -class JITTag(object): +class JITTag: """This is a template Tag which is only evaulated on replacement. It is usefull for situations where not all all of this functions input vars are available just yet. From dba06f2a2218085ebd3ca32b9fee75072e9d0936 Mon Sep 17 00:00:00 2001 From: Erwin Hager Date: Fri, 11 Dec 2020 16:11:49 +0100 Subject: [PATCH 069/118] import reload function from importlib --- uweb3/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index f10f8e3b..10c8da9a 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -21,6 +21,7 @@ from .pagemaker import PageMaker, decorators, WebsocketPageMaker, DebuggingPageMaker, LoginMixin from .model import SettingsManager from .libs.safestring import HTMLsafestring, JSONsafestring, JsonEncoder, Basesafestring +from importlib import reload class Error(Exception): """Superclass used for inheritance and external exception handling.""" From ffb106a1a5db4223d61408426f9bf1d03135cca2 Mon Sep 17 00:00:00 2001 From: Erwin Hager Date: Fri, 11 Dec 2020 16:12:17 +0100 Subject: [PATCH 070/118] fix typo in _GetSearchQuery connection arg --- uweb3/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uweb3/model.py b/uweb3/model.py index 3f44f3c5..7ee7618a 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -1230,7 +1230,7 @@ def Save(self, save_foreign=False): # pylint: enable=W0221 @classmethod - def _GetSearchQuery(cls, connetion, tables, search): + def _GetSearchQuery(cls, connection, tables, search): """Extracts table information from the searchable columns list.""" conditions = [] like = 'like "%%%s%%"' % connection.EscapeValues(search.strip())[1:-1] From 532082d8ac10d8abe64cb0c43e484e5e26a6f031 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 14 Dec 2020 10:36:45 +0100 Subject: [PATCH 071/118] fix some misnamings --- uweb3/model.py | 7 ++++--- uweb3/pagemaker/__init__.py | 2 +- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/uweb3/model.py b/uweb3/model.py index 7ee7618a..7bc04490 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -1245,12 +1245,13 @@ def _GetSearchQuery(cls, connection, tables, search): if (othertable != thistable and othertable not in tables): fkey = cls._FOREIGN_RELATIONS.get(classname, False) - key = table._PRIMARY_KEY + key = othertable._PRIMARY_KEY if fkey and fkey.get('LookupKey', False): key = fkey.get('LookupKey') elif getattr(table, "RecordKey", None): - key = table.RecordKey() + key = othertable.RecordKey() # add the cross table join + #TODO use referenced field, instead of just the othertable name conditions.append('`%s`.`%s` = `%s`.`%s' % (thistable, othertable, othertable, @@ -1394,7 +1395,7 @@ def List(cls, connection, conditions=None, limit=None, offset=None, if type(conditions) == list: conditions.extend(searchconditions) else: - newconditions.append(conditions) + searchconditions.append(conditions) conditions = searchconditions else: conditions = searchconditions diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index f134fba3..6092e4a1 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -337,7 +337,7 @@ def LoadModules(cls, routes='routes/*.py'): classlist = pyclbr.readmodule_ex(module) for name, data in classlist.items(): if hasattr(data, 'super') and 'PageMaker' in data.super[0]: - module = __import__(f, fromlist=[name]) + module = __import__(file, fromlist=[name]) bases.append(getattr(module, name)) return bases From b6dddab3cbca06d0c30d4ad31262222526af9fe1 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 15 Dec 2020 10:56:11 +0100 Subject: [PATCH 072/118] string compare only once, for static routes --- uweb3/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 10c8da9a..9b852511 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -222,7 +222,11 @@ def __call__(self, env, start_response): executing_path=self.executing_path) response = pagemaker_instance.InternalServerError(*sys.exc_info()) - if method != 'Static': + static = False + if method == 'Static': + static = True + + if not static: if not isinstance(response, Response): # print('Upgrade response to Response class: %s' % type(response)) req.response.text = response @@ -243,7 +247,7 @@ def __call__(self, env, start_response): pagemaker_instance._CSPheaders() # provide users with a _PostRequest method to overide too - if method != 'Static' and hasattr(pagemaker_instance, 'PostRequest'): + if not static and hasattr(pagemaker_instance, 'PostRequest'): response = pagemaker_instance.PostRequest(response) # we should at least send out something to make sure we are wsgi compliant. From 04ee7f7d70c5752f8ba28607a91f923fb13bcd1c Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 16 Dec 2020 16:16:17 +0100 Subject: [PATCH 073/118] allow dots in indice names for the templateparser. --- uweb3/templateparser.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index aa2e6364..f500ee6d 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -316,7 +316,7 @@ class Template(list): # For a full tag syntax explanation, refer to the TAG regex in TemplateTag. TAG = re.compile(""" (\[\w+ # Tag start and alphanum tagname - (?:(?::[\w-]+)+)? # 0+ indices, alphanum with dashes + (?:(?::[\w\-\.]+)+)? # 0+ indices, alphanum with dashes (?:(?:\|[\w-]+ # 0+ functions, alphanum with dashes (?:\([^()]*?\))? # closure parentheses and arguments )+)? # end of function block @@ -801,7 +801,7 @@ class TemplateTag: TAG = re.compile(""" \[ # Tag starts with opening bracket (\w+) # Capture tagname (1+ alphanum length) - ((?::[\w-]+)+)? # Capture 0+ indices (1+ alphanum+dashes length) + ((?::[\w\-\.]+)+)? # Capture 0+ indices (1+ alphanum+dashes length) ((?:\|[\w-]+ # Capture 0+ functions (1+ alphanum+dashes length) (?:\([^()]*?\))? # Functions may be closures with arguments. )+)? # // end of optional functions From 9b931e53566a29d0de8de9bece533f0a4d25767a Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 2 Feb 2021 15:15:32 +0100 Subject: [PATCH 074/118] add In keyword to Valid AST commands in eval mode --- uweb3/templateparser.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index f500ee6d..8cdb631a 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -116,7 +116,7 @@ def values(self): 'operators': (ast.Module, ast.Expr, ast.Load, ast.Expression, ast.Add, ast.And, ast.Sub, ast.UnaryOp, ast.Num, ast.BinOp, ast.Mult, ast.Gt, ast.GtE, ast.Div, ast.Pow, ast.BitOr, ast.BitAnd, ast.BitXor, ast.Lt, ast.LtE, - ast.USub, ast.UAdd, ast.FloorDiv, ast.Mod, ast.LShift, + ast.USub, ast.UAdd, ast.FloorDiv, ast.Mod, ast.In, ast.LShift, ast.RShift, ast.Invert, ast.Call, ast.Name, ast.Compare, ast.Eq, ast.NotEq, ast.Not, ast.Or, ast.BoolOp, ast.Str)} From 50db224665860ba0062493655cb36b32c1485a43 Mon Sep 17 00:00:00 2001 From: Jan Date: Thu, 4 Feb 2021 18:51:17 +0100 Subject: [PATCH 075/118] Static files can simly be read as binary --- uweb3/pagemaker/__init__.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 6092e4a1..bf37e8f5 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -402,12 +402,9 @@ def Static(self, rel_path): content_type, _encoding = mimetypes.guess_type(abs_path) if not content_type: content_type = 'text/plain' - binary = False - if not content_type.startswith('text/'): - binary = True - with open(abs_path, 'rb' if binary else 'r') as staticfile: - mtime = os.path.getmtime(abs_path) - length = os.path.getsize(abs_path) + mtime = os.path.getmtime(abs_path) + length = os.path.getsize(abs_path) + with open(abs_path, 'rb') as staticfile: cache_days = self.CACHE_DURATION.get(content_type, 0) expires = datetime.datetime.utcnow() + datetime.timedelta(cache_days) return response.Response(content=staticfile.read(), From 2b027e927a084f83f8aff1796d08f91a89c1f096 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 7 Feb 2021 13:59:15 +0100 Subject: [PATCH 076/118] license update to GPL v3, add build script --- pyproject.toml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 pyproject.toml diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 00000000..374b58cb --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,6 @@ +[build-system] +requires = [ + "setuptools>=42", + "wheel" +] +build-backend = "setuptools.build_meta" From a7dc9b4794324e02269f3e474c738d26ccb21cdb Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 7 Feb 2021 13:59:24 +0100 Subject: [PATCH 077/118] license update to GPL v3, add build script --- LICENSE | 675 +++++++++++++++++++++++++++++++++++++++++++++++++++++- README.md | 3 +- setup.py | 3 +- 3 files changed, 676 insertions(+), 5 deletions(-) diff --git a/LICENSE b/LICENSE index 7d2ba457..f288702d 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,674 @@ -Copyright (c) 2012, Underdark + GNU GENERAL PUBLIC LICENSE + Version 3, 29 June 2007 -Permission to use, copy, modify, and/or distribute this software for any purpose with or without fee is hereby granted, provided that the above copyright notice and this permission notice appear in all copies. + Copyright (C) 2007 Free Software Foundation, Inc. + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. -THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + Preamble + + The GNU General Public License is a free, copyleft license for +software and other kinds of works. + + The licenses for most software and other practical works are designed +to take away your freedom to share and change the works. By contrast, +the GNU General Public License is intended to guarantee your freedom to +share and change all versions of a program--to make sure it remains free +software for all its users. We, the Free Software Foundation, use the +GNU General Public License for most of our software; it applies also to +any other work released this way by its authors. You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +them if you wish), that you receive source code or can get it if you +want it, that you can change the software or use pieces of it in new +free programs, and that you know you can do these things. + + To protect your rights, we need to prevent others from denying you +these rights or asking you to surrender the rights. Therefore, you have +certain responsibilities if you distribute copies of the software, or if +you modify it: responsibilities to respect the freedom of others. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must pass on to the recipients the same +freedoms that you received. You must make sure that they, too, receive +or can get the source code. And you must show them these terms so they +know their rights. + + Developers that use the GNU GPL protect your rights with two steps: +(1) assert copyright on the software, and (2) offer you this License +giving you legal permission to copy, distribute and/or modify it. + + For the developers' and authors' protection, the GPL clearly explains +that there is no warranty for this free software. For both users' and +authors' sake, the GPL requires that modified versions be marked as +changed, so that their problems will not be attributed erroneously to +authors of previous versions. + + Some devices are designed to deny users access to install or run +modified versions of the software inside them, although the manufacturer +can do so. This is fundamentally incompatible with the aim of +protecting users' freedom to change the software. The systematic +pattern of such abuse occurs in the area of products for individuals to +use, which is precisely where it is most unacceptable. Therefore, we +have designed this version of the GPL to prohibit the practice for those +products. If such problems arise substantially in other domains, we +stand ready to extend this provision to those domains in future versions +of the GPL, as needed to protect the freedom of users. + + Finally, every program is threatened constantly by software patents. +States should not allow patents to restrict development and use of +software on general-purpose computers, but in those that do, we wish to +avoid the special danger that patents applied to a free program could +make it effectively proprietary. To prevent this, the GPL assures that +patents cannot be used to render the program non-free. + + The precise terms and conditions for copying, distribution and +modification follow. + + TERMS AND CONDITIONS + + 0. Definitions. + + "This License" refers to version 3 of the GNU General Public License. + + "Copyright" also means copyright-like laws that apply to other kinds of +works, such as semiconductor masks. + + "The Program" refers to any copyrightable work licensed under this +License. Each licensee is addressed as "you". "Licensees" and +"recipients" may be individuals or organizations. + + To "modify" a work means to copy from or adapt all or part of the work +in a fashion requiring copyright permission, other than the making of an +exact copy. The resulting work is called a "modified version" of the +earlier work or a work "based on" the earlier work. + + A "covered work" means either the unmodified Program or a work based +on the Program. + + To "propagate" a work means to do anything with it that, without +permission, would make you directly or secondarily liable for +infringement under applicable copyright law, except executing it on a +computer or modifying a private copy. Propagation includes copying, +distribution (with or without modification), making available to the +public, and in some countries other activities as well. + + To "convey" a work means any kind of propagation that enables other +parties to make or receive copies. Mere interaction with a user through +a computer network, with no transfer of a copy, is not conveying. + + An interactive user interface displays "Appropriate Legal Notices" +to the extent that it includes a convenient and prominently visible +feature that (1) displays an appropriate copyright notice, and (2) +tells the user that there is no warranty for the work (except to the +extent that warranties are provided), that licensees may convey the +work under this License, and how to view a copy of this License. If +the interface presents a list of user commands or options, such as a +menu, a prominent item in the list meets this criterion. + + 1. Source Code. + + The "source code" for a work means the preferred form of the work +for making modifications to it. "Object code" means any non-source +form of a work. + + A "Standard Interface" means an interface that either is an official +standard defined by a recognized standards body, or, in the case of +interfaces specified for a particular programming language, one that +is widely used among developers working in that language. + + The "System Libraries" of an executable work include anything, other +than the work as a whole, that (a) is included in the normal form of +packaging a Major Component, but which is not part of that Major +Component, and (b) serves only to enable use of the work with that +Major Component, or to implement a Standard Interface for which an +implementation is available to the public in source code form. A +"Major Component", in this context, means a major essential component +(kernel, window system, and so on) of the specific operating system +(if any) on which the executable work runs, or a compiler used to +produce the work, or an object code interpreter used to run it. + + The "Corresponding Source" for a work in object code form means all +the source code needed to generate, install, and (for an executable +work) run the object code and to modify the work, including scripts to +control those activities. However, it does not include the work's +System Libraries, or general-purpose tools or generally available free +programs which are used unmodified in performing those activities but +which are not part of the work. For example, Corresponding Source +includes interface definition files associated with source files for +the work, and the source code for shared libraries and dynamically +linked subprograms that the work is specifically designed to require, +such as by intimate data communication or control flow between those +subprograms and other parts of the work. + + The Corresponding Source need not include anything that users +can regenerate automatically from other parts of the Corresponding +Source. + + The Corresponding Source for a work in source code form is that +same work. + + 2. Basic Permissions. + + All rights granted under this License are granted for the term of +copyright on the Program, and are irrevocable provided the stated +conditions are met. This License explicitly affirms your unlimited +permission to run the unmodified Program. The output from running a +covered work is covered by this License only if the output, given its +content, constitutes a covered work. This License acknowledges your +rights of fair use or other equivalent, as provided by copyright law. + + You may make, run and propagate covered works that you do not +convey, without conditions so long as your license otherwise remains +in force. You may convey covered works to others for the sole purpose +of having them make modifications exclusively for you, or provide you +with facilities for running those works, provided that you comply with +the terms of this License in conveying all material for which you do +not control copyright. Those thus making or running the covered works +for you must do so exclusively on your behalf, under your direction +and control, on terms that prohibit them from making any copies of +your copyrighted material outside their relationship with you. + + Conveying under any other circumstances is permitted solely under +the conditions stated below. Sublicensing is not allowed; section 10 +makes it unnecessary. + + 3. Protecting Users' Legal Rights From Anti-Circumvention Law. + + No covered work shall be deemed part of an effective technological +measure under any applicable law fulfilling obligations under article +11 of the WIPO copyright treaty adopted on 20 December 1996, or +similar laws prohibiting or restricting circumvention of such +measures. + + When you convey a covered work, you waive any legal power to forbid +circumvention of technological measures to the extent such circumvention +is effected by exercising rights under this License with respect to +the covered work, and you disclaim any intention to limit operation or +modification of the work as a means of enforcing, against the work's +users, your or third parties' legal rights to forbid circumvention of +technological measures. + + 4. Conveying Verbatim Copies. + + You may convey verbatim copies of the Program's source code as you +receive it, in any medium, provided that you conspicuously and +appropriately publish on each copy an appropriate copyright notice; +keep intact all notices stating that this License and any +non-permissive terms added in accord with section 7 apply to the code; +keep intact all notices of the absence of any warranty; and give all +recipients a copy of this License along with the Program. + + You may charge any price or no price for each copy that you convey, +and you may offer support or warranty protection for a fee. + + 5. Conveying Modified Source Versions. + + You may convey a work based on the Program, or the modifications to +produce it from the Program, in the form of source code under the +terms of section 4, provided that you also meet all of these conditions: + + a) The work must carry prominent notices stating that you modified + it, and giving a relevant date. + + b) The work must carry prominent notices stating that it is + released under this License and any conditions added under section + 7. This requirement modifies the requirement in section 4 to + "keep intact all notices". + + c) You must license the entire work, as a whole, under this + License to anyone who comes into possession of a copy. This + License will therefore apply, along with any applicable section 7 + additional terms, to the whole of the work, and all its parts, + regardless of how they are packaged. This License gives no + permission to license the work in any other way, but it does not + invalidate such permission if you have separately received it. + + d) If the work has interactive user interfaces, each must display + Appropriate Legal Notices; however, if the Program has interactive + interfaces that do not display Appropriate Legal Notices, your + work need not make them do so. + + A compilation of a covered work with other separate and independent +works, which are not by their nature extensions of the covered work, +and which are not combined with it such as to form a larger program, +in or on a volume of a storage or distribution medium, is called an +"aggregate" if the compilation and its resulting copyright are not +used to limit the access or legal rights of the compilation's users +beyond what the individual works permit. Inclusion of a covered work +in an aggregate does not cause this License to apply to the other +parts of the aggregate. + + 6. Conveying Non-Source Forms. + + You may convey a covered work in object code form under the terms +of sections 4 and 5, provided that you also convey the +machine-readable Corresponding Source under the terms of this License, +in one of these ways: + + a) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by the + Corresponding Source fixed on a durable physical medium + customarily used for software interchange. + + b) Convey the object code in, or embodied in, a physical product + (including a physical distribution medium), accompanied by a + written offer, valid for at least three years and valid for as + long as you offer spare parts or customer support for that product + model, to give anyone who possesses the object code either (1) a + copy of the Corresponding Source for all the software in the + product that is covered by this License, on a durable physical + medium customarily used for software interchange, for a price no + more than your reasonable cost of physically performing this + conveying of source, or (2) access to copy the + Corresponding Source from a network server at no charge. + + c) Convey individual copies of the object code with a copy of the + written offer to provide the Corresponding Source. This + alternative is allowed only occasionally and noncommercially, and + only if you received the object code with such an offer, in accord + with subsection 6b. + + d) Convey the object code by offering access from a designated + place (gratis or for a charge), and offer equivalent access to the + Corresponding Source in the same way through the same place at no + further charge. You need not require recipients to copy the + Corresponding Source along with the object code. If the place to + copy the object code is a network server, the Corresponding Source + may be on a different server (operated by you or a third party) + that supports equivalent copying facilities, provided you maintain + clear directions next to the object code saying where to find the + Corresponding Source. Regardless of what server hosts the + Corresponding Source, you remain obligated to ensure that it is + available for as long as needed to satisfy these requirements. + + e) Convey the object code using peer-to-peer transmission, provided + you inform other peers where the object code and Corresponding + Source of the work are being offered to the general public at no + charge under subsection 6d. + + A separable portion of the object code, whose source code is excluded +from the Corresponding Source as a System Library, need not be +included in conveying the object code work. + + A "User Product" is either (1) a "consumer product", which means any +tangible personal property which is normally used for personal, family, +or household purposes, or (2) anything designed or sold for incorporation +into a dwelling. In determining whether a product is a consumer product, +doubtful cases shall be resolved in favor of coverage. For a particular +product received by a particular user, "normally used" refers to a +typical or common use of that class of product, regardless of the status +of the particular user or of the way in which the particular user +actually uses, or expects or is expected to use, the product. A product +is a consumer product regardless of whether the product has substantial +commercial, industrial or non-consumer uses, unless such uses represent +the only significant mode of use of the product. + + "Installation Information" for a User Product means any methods, +procedures, authorization keys, or other information required to install +and execute modified versions of a covered work in that User Product from +a modified version of its Corresponding Source. The information must +suffice to ensure that the continued functioning of the modified object +code is in no case prevented or interfered with solely because +modification has been made. + + If you convey an object code work under this section in, or with, or +specifically for use in, a User Product, and the conveying occurs as +part of a transaction in which the right of possession and use of the +User Product is transferred to the recipient in perpetuity or for a +fixed term (regardless of how the transaction is characterized), the +Corresponding Source conveyed under this section must be accompanied +by the Installation Information. But this requirement does not apply +if neither you nor any third party retains the ability to install +modified object code on the User Product (for example, the work has +been installed in ROM). + + The requirement to provide Installation Information does not include a +requirement to continue to provide support service, warranty, or updates +for a work that has been modified or installed by the recipient, or for +the User Product in which it has been modified or installed. Access to a +network may be denied when the modification itself materially and +adversely affects the operation of the network or violates the rules and +protocols for communication across the network. + + Corresponding Source conveyed, and Installation Information provided, +in accord with this section must be in a format that is publicly +documented (and with an implementation available to the public in +source code form), and must require no special password or key for +unpacking, reading or copying. + + 7. Additional Terms. + + "Additional permissions" are terms that supplement the terms of this +License by making exceptions from one or more of its conditions. +Additional permissions that are applicable to the entire Program shall +be treated as though they were included in this License, to the extent +that they are valid under applicable law. If additional permissions +apply only to part of the Program, that part may be used separately +under those permissions, but the entire Program remains governed by +this License without regard to the additional permissions. + + When you convey a copy of a covered work, you may at your option +remove any additional permissions from that copy, or from any part of +it. (Additional permissions may be written to require their own +removal in certain cases when you modify the work.) You may place +additional permissions on material, added by you to a covered work, +for which you have or can give appropriate copyright permission. + + Notwithstanding any other provision of this License, for material you +add to a covered work, you may (if authorized by the copyright holders of +that material) supplement the terms of this License with terms: + + a) Disclaiming warranty or limiting liability differently from the + terms of sections 15 and 16 of this License; or + + b) Requiring preservation of specified reasonable legal notices or + author attributions in that material or in the Appropriate Legal + Notices displayed by works containing it; or + + c) Prohibiting misrepresentation of the origin of that material, or + requiring that modified versions of such material be marked in + reasonable ways as different from the original version; or + + d) Limiting the use for publicity purposes of names of licensors or + authors of the material; or + + e) Declining to grant rights under trademark law for use of some + trade names, trademarks, or service marks; or + + f) Requiring indemnification of licensors and authors of that + material by anyone who conveys the material (or modified versions of + it) with contractual assumptions of liability to the recipient, for + any liability that these contractual assumptions directly impose on + those licensors and authors. + + All other non-permissive additional terms are considered "further +restrictions" within the meaning of section 10. If the Program as you +received it, or any part of it, contains a notice stating that it is +governed by this License along with a term that is a further +restriction, you may remove that term. If a license document contains +a further restriction but permits relicensing or conveying under this +License, you may add to a covered work material governed by the terms +of that license document, provided that the further restriction does +not survive such relicensing or conveying. + + If you add terms to a covered work in accord with this section, you +must place, in the relevant source files, a statement of the +additional terms that apply to those files, or a notice indicating +where to find the applicable terms. + + Additional terms, permissive or non-permissive, may be stated in the +form of a separately written license, or stated as exceptions; +the above requirements apply either way. + + 8. Termination. + + You may not propagate or modify a covered work except as expressly +provided under this License. Any attempt otherwise to propagate or +modify it is void, and will automatically terminate your rights under +this License (including any patent licenses granted under the third +paragraph of section 11). + + However, if you cease all violation of this License, then your +license from a particular copyright holder is reinstated (a) +provisionally, unless and until the copyright holder explicitly and +finally terminates your license, and (b) permanently, if the copyright +holder fails to notify you of the violation by some reasonable means +prior to 60 days after the cessation. + + Moreover, your license from a particular copyright holder is +reinstated permanently if the copyright holder notifies you of the +violation by some reasonable means, this is the first time you have +received notice of violation of this License (for any work) from that +copyright holder, and you cure the violation prior to 30 days after +your receipt of the notice. + + Termination of your rights under this section does not terminate the +licenses of parties who have received copies or rights from you under +this License. If your rights have been terminated and not permanently +reinstated, you do not qualify to receive new licenses for the same +material under section 10. + + 9. Acceptance Not Required for Having Copies. + + You are not required to accept this License in order to receive or +run a copy of the Program. Ancillary propagation of a covered work +occurring solely as a consequence of using peer-to-peer transmission +to receive a copy likewise does not require acceptance. However, +nothing other than this License grants you permission to propagate or +modify any covered work. These actions infringe copyright if you do +not accept this License. Therefore, by modifying or propagating a +covered work, you indicate your acceptance of this License to do so. + + 10. Automatic Licensing of Downstream Recipients. + + Each time you convey a covered work, the recipient automatically +receives a license from the original licensors, to run, modify and +propagate that work, subject to this License. You are not responsible +for enforcing compliance by third parties with this License. + + An "entity transaction" is a transaction transferring control of an +organization, or substantially all assets of one, or subdividing an +organization, or merging organizations. If propagation of a covered +work results from an entity transaction, each party to that +transaction who receives a copy of the work also receives whatever +licenses to the work the party's predecessor in interest had or could +give under the previous paragraph, plus a right to possession of the +Corresponding Source of the work from the predecessor in interest, if +the predecessor has it or can get it with reasonable efforts. + + You may not impose any further restrictions on the exercise of the +rights granted or affirmed under this License. For example, you may +not impose a license fee, royalty, or other charge for exercise of +rights granted under this License, and you may not initiate litigation +(including a cross-claim or counterclaim in a lawsuit) alleging that +any patent claim is infringed by making, using, selling, offering for +sale, or importing the Program or any portion of it. + + 11. Patents. + + A "contributor" is a copyright holder who authorizes use under this +License of the Program or a work on which the Program is based. The +work thus licensed is called the contributor's "contributor version". + + A contributor's "essential patent claims" are all patent claims +owned or controlled by the contributor, whether already acquired or +hereafter acquired, that would be infringed by some manner, permitted +by this License, of making, using, or selling its contributor version, +but do not include claims that would be infringed only as a +consequence of further modification of the contributor version. For +purposes of this definition, "control" includes the right to grant +patent sublicenses in a manner consistent with the requirements of +this License. + + Each contributor grants you a non-exclusive, worldwide, royalty-free +patent license under the contributor's essential patent claims, to +make, use, sell, offer for sale, import and otherwise run, modify and +propagate the contents of its contributor version. + + In the following three paragraphs, a "patent license" is any express +agreement or commitment, however denominated, not to enforce a patent +(such as an express permission to practice a patent or covenant not to +sue for patent infringement). To "grant" such a patent license to a +party means to make such an agreement or commitment not to enforce a +patent against the party. + + If you convey a covered work, knowingly relying on a patent license, +and the Corresponding Source of the work is not available for anyone +to copy, free of charge and under the terms of this License, through a +publicly available network server or other readily accessible means, +then you must either (1) cause the Corresponding Source to be so +available, or (2) arrange to deprive yourself of the benefit of the +patent license for this particular work, or (3) arrange, in a manner +consistent with the requirements of this License, to extend the patent +license to downstream recipients. "Knowingly relying" means you have +actual knowledge that, but for the patent license, your conveying the +covered work in a country, or your recipient's use of the covered work +in a country, would infringe one or more identifiable patents in that +country that you have reason to believe are valid. + + If, pursuant to or in connection with a single transaction or +arrangement, you convey, or propagate by procuring conveyance of, a +covered work, and grant a patent license to some of the parties +receiving the covered work authorizing them to use, propagate, modify +or convey a specific copy of the covered work, then the patent license +you grant is automatically extended to all recipients of the covered +work and works based on it. + + A patent license is "discriminatory" if it does not include within +the scope of its coverage, prohibits the exercise of, or is +conditioned on the non-exercise of one or more of the rights that are +specifically granted under this License. You may not convey a covered +work if you are a party to an arrangement with a third party that is +in the business of distributing software, under which you make payment +to the third party based on the extent of your activity of conveying +the work, and under which the third party grants, to any of the +parties who would receive the covered work from you, a discriminatory +patent license (a) in connection with copies of the covered work +conveyed by you (or copies made from those copies), or (b) primarily +for and in connection with specific products or compilations that +contain the covered work, unless you entered into that arrangement, +or that patent license was granted, prior to 28 March 2007. + + Nothing in this License shall be construed as excluding or limiting +any implied license or other defenses to infringement that may +otherwise be available to you under applicable patent law. + + 12. No Surrender of Others' Freedom. + + If conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot convey a +covered work so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you may +not convey it at all. For example, if you agree to terms that obligate you +to collect a royalty for further conveying from those to whom you convey +the Program, the only way you could satisfy both those terms and this +License would be to refrain entirely from conveying the Program. + + 13. Use with the GNU Affero General Public License. + + Notwithstanding any other provision of this License, you have +permission to link or combine any covered work with a work licensed +under version 3 of the GNU Affero General Public License into a single +combined work, and to convey the resulting work. The terms of this +License will continue to apply to the part which is the covered work, +but the special requirements of the GNU Affero General Public License, +section 13, concerning interaction through a network will apply to the +combination as such. + + 14. Revised Versions of this License. + + The Free Software Foundation may publish revised and/or new versions of +the GNU General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + + Each version is given a distinguishing version number. If the +Program specifies that a certain numbered version of the GNU General +Public License "or any later version" applies to it, you have the +option of following the terms and conditions either of that numbered +version or of any later version published by the Free Software +Foundation. If the Program does not specify a version number of the +GNU General Public License, you may choose any version ever published +by the Free Software Foundation. + + If the Program specifies that a proxy can decide which future +versions of the GNU General Public License can be used, that proxy's +public statement of acceptance of a version permanently authorizes you +to choose that version for the Program. + + Later license versions may give you additional or different +permissions. However, no additional obligations are imposed on any +author or copyright holder as a result of your choosing to follow a +later version. + + 15. Disclaimer of Warranty. + + THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY +APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT +HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY +OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, +THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR +PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM +IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF +ALL NECESSARY SERVICING, REPAIR OR CORRECTION. + + 16. Limitation of Liability. + + IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS +THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY +GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE +USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF +DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD +PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), +EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF +SUCH DAMAGES. + + 17. Interpretation of Sections 15 and 16. + + If the disclaimer of warranty and limitation of liability provided +above cannot be given local legal effect according to their terms, +reviewing courts shall apply local law that most closely approximates +an absolute waiver of all civil liability in connection with the +Program, unless a warranty or assumption of liability accompanies a +copy of the Program in return for a fee. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +state the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see . + +Also add information on how to contact you by electronic and paper mail. + + If the program does terminal interaction, make it output a short +notice like this when it starts in an interactive mode: + + Copyright (C) + This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, your program's commands +might be different; for a GUI interface, you would use an "about box". + + You should also get your employer (if you work as a programmer) or school, +if any, to sign a "copyright disclaimer" for the program, if necessary. +For more information on this, and how to apply and follow the GNU GPL, see +. + + The GNU General Public License does not permit incorporating your program +into proprietary programs. If your program is a subroutine library, you +may consider it more useful to permit linking proprietary applications with +the library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. But first, please read +. diff --git a/README.md b/README.md index 2db07def..8cab0bad 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ Since µWeb inception we have used it for many projects, and while it did its job, there were plenty of rough edges. This new version intends to remove those and pull it into the current age. +µWeb3 is free software, distributed under the terms of the [GNU] General Public License as published by the Free Software Foundation, version 3 of the License (or any later version). For more information, see the file LICENSE + # Notable changes * wsgi complaint interface @@ -16,7 +18,6 @@ Since µWeb inception we have used it for many projects, and while it did its jo The following example applications for uWeb3 exist: * [uWeb3-scaffold](https://github.com/underdarknl/uweb3scaffold): This is an empty project which you can fork to start your own website -* [uWeb3-logviewer](https://github.com/edelooff/uWeb3-logviewer): This allows you to view and search in the logs generated by all µWeb and µWeb3 applications. # µWeb3 installation diff --git a/setup.py b/setup.py index 62f70075..1efb1994 100644 --- a/setup.py +++ b/setup.py @@ -45,4 +45,5 @@ def version(): packages=find_packages(), include_package_data=True, zip_safe=False, - install_requires=REQUIREMENTS) + install_requires=REQUIREMENTS, + python_requires='>=3.5') From 2a83effa66494c8fd8439a517e5bfea6ca328859 Mon Sep 17 00:00:00 2001 From: Jan Date: Sun, 7 Feb 2021 14:03:36 +0100 Subject: [PATCH 078/118] fix setup.py long_description_file is not actualy a useful key --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 1efb1994..b15bf21a 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ def version(): name='uWeb3', version=version(), description='uWeb, python3, uswgi compatible micro web platform', - long_description_file = 'README.md', + long_description = 'file: README.md', long_description_content_type = 'text/markdown', license='ISC', classifiers=[ From e11b65bd0984b33449ad0517d9b4d0b1e9f950ea Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 15 Feb 2021 10:01:07 +0100 Subject: [PATCH 079/118] improve handling of static and template files, make sure we dont allow path traversal, and are Windows compatible. Set defaultEncoding for template files to utf-8, and add methods to overwrite this when needed. some python3 cleanup. Add method to overwrite the AST whitelist. --- setup.py | 13 ++++++------- uweb3/__init__.py | 2 +- uweb3/pagemaker/__init__.py | 33 ++++++++++----------------------- uweb3/templateparser.py | 36 +++++++++++++++++++++++++++--------- 4 files changed, 44 insertions(+), 40 deletions(-) diff --git a/setup.py b/setup.py index b15bf21a..5a89e3ea 100644 --- a/setup.py +++ b/setup.py @@ -9,26 +9,25 @@ 'pytz' ] -# 'sqlalchemy', -# 'werkzeug', - def description(): + """Returns the contents of the README.md file as description information.""" with open(os.path.join(os.path.dirname(__file__), 'README.md')) as r_file: return r_file.read() def version(): + """Returns the version of the library as read from the __init__.py file""" main_lib = os.path.join(os.path.dirname(__file__), 'uweb3', '__init__.py') with open(main_lib) as v_file: return re.match(".*__version__ = '(.*?)'", v_file.read(), re.S).group(1) setup( - name='uWeb3', + name='uWebthree', version=version(), description='uWeb, python3, uswgi compatible micro web platform', - long_description = 'file: README.md', - long_description_content_type = 'text/markdown', + long_description=description(), + long_description_content_type='text/markdown', license='ISC', classifiers=[ 'Development Status :: 4 - Beta', @@ -36,6 +35,7 @@ def version(): 'Environment :: Web Environment', 'License :: OSI Approved :: ISC License (ISCL)', 'Operating System :: POSIX :: Linux', + 'Programming Language :: Python', 'Programming Language :: Python :: 3', ], author='Jan Klopper', @@ -44,6 +44,5 @@ def version(): keywords='minimal python web framework', packages=find_packages(), include_package_data=True, - zip_safe=False, install_requires=REQUIREMENTS, python_requires='>=3.5') diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 9b852511..87421a91 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """µWeb3 Framework""" -__version__ = '3.0' +__version__ = '3.0.1' # Standard modules import configparser diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index bf37e8f5..5dc89594 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -171,7 +171,9 @@ class Base: def __init__(self): self.persistent = self.PERSISTENT - # clean up any request tags in the template parser + # clean up any request tags in the template parser, We do this in the init + # because due to crashes we might not have triggered any __del__ or similar + # end of request code. if '__parser' in self.persistent: self.persistent.Get('__parser').ClearRequestTags() @@ -356,27 +358,10 @@ def __SetupPaths(cls, executing_path): directory is used as the working directory. Then, the module constant TEMPLATE_DIR is used to define class constants from. """ - # Unfortunately, mod_python does not always support retrieving the caller - # filename using sys.modules. In those cases we need to query the stack. - # pylint: disable=W0212 - try: - local_file = os.path.abspath(sys.modules[cls.__module__].__file__) - except KeyError: - # This happens for old-style mod_python solutions: The pages file is - # imported through the mechanics of mod_pythoif '__mysql' not in self.persistent: (not package imports) and - # isn't known in sys modules. We use the CPython implementation details - # to get the correct executing file. - frame = sys._getframe() - initial = frame.f_code.co_filename - # pylint: enable=W0212 - while initial == frame.f_code.co_filename: - if not frame.f_back: - break # This happens during exception handling of DebuggingPageMaker - frame = frame.f_back - local_file = frame.f_code.co_filename + local_file = os.path.abspath(sys.modules[cls.__module__].__file__) cls.LOCAL_DIR = cls_dir = executing_path - cls.PUBLIC_DIR = os.path.join(cls_dir, cls.PUBLIC_DIR) - cls.TEMPLATE_DIR = os.path.join(cls_dir, cls.TEMPLATE_DIR) + cls.PUBLIC_DIR = os.path.realpath(os.path.join(cls_dir, cls.PUBLIC_DIR)) + cls.TEMPLATE_DIR = os.path.realpath(os.path.join(cls_dir, cls.TEMPLATE_DIR)) def Static(self, rel_path): """Provides a handler for static content. @@ -396,8 +381,10 @@ def Static(self, rel_path): Page: contains the content and mimetype of the requested file, or a 404 page if the file was not available on the local path. """ - rel_path = os.path.abspath(os.path.join(os.path.sep, rel_path))[1:] - abs_path = os.path.join(self.PUBLIC_DIR, rel_path) + + abs_path = os.path.realpath(os.path.join(self.PUBLIC_DIR, rel_path)) + if os.path.commonprefix((abs_path, self.PUBLIC_DIR)) != self.PUBLIC_DIR: + return self._StaticNotFound(rel_path) try: content_type, _encoding = mimetypes.guess_type(abs_path) if not content_type: diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index 8cdb631a..802ea96c 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -142,7 +142,7 @@ class Parser(dict): providing the `RegisterFunction` method to add or replace functions in this module constant. """ - def __init__(self, path='.', templates=(), noparse=False): + def __init__(self, path='.', templates=(), noparse=False, templateEncoding='utf-8'): """Initializes a Parser instance. This sets up the template directory and preloads any templates given. @@ -156,12 +156,13 @@ def __init__(self, path='.', templates=(), noparse=False): Skip parsing the templates to output, instead return their structure and replaced values """ - super(Parser, self).__init__() + super().__init__() self.template_dir = path self.noparse = noparse self.tags = {} self.requesttags = {} self.astvisitor = AstVisitor(EVALWHITELIST) + self.templateEncoding = templateEncoding for template in templates: self.AddTemplate(template) @@ -204,9 +205,11 @@ def AddTemplate(self, location, name=None): Raises: TemplateReadError: When the template file cannot be read """ + template_path = os.path.realpath(os.path.join(self.template_dir, location)) + if os.path.commonprefix((template_path, self.template_dir)) != self.template_dir: + raise TemplateReadError('Could not load template %r' % template_path) try: - template_path = os.path.join(self.template_dir, location) - self[name or location] = FileTemplate(template_path, parser=self) + self[name or location] = FileTemplate(template_path, parser=self, encoding=None) except IOError: raise TemplateReadError('Could not load template %r' % template_path) @@ -307,6 +310,18 @@ def ClearRequestTags(self): completed request""" self.requesttags = {} + def SetTemplateEncoding(self, templateEncoding='utf-8'): + """Allows the user to set the templateEncoding for this parser instance's + templates. Any template reads, and reloads will be attempted with this + encoding.""" + self.templateEncoding = templateEncoding + + def SetEvalWhitelist(self, evalwhitelist=None): + """Allows the user to set the Eval Whitelist which limits the python + operations allowed within this templateParsers Context. These are usually + triggered by If/Elif conditions and the like.""" + self.astvisitor = AstVisitor(evalwhitelist) + TemplateReadError = TemplateReadError @@ -535,7 +550,7 @@ def _VerifyOpenScope(self, scope_cls): class FileTemplate(Template): """Template class that loads from file.""" - def __init__(self, template_path, parser=None): + def __init__(self, template_path, parser=None, encoding='utf-8'): """Initializes a FileTemplate based on a given template path. Arguments: @@ -547,12 +562,15 @@ def __init__(self, template_path, parser=None): adding files to the current template. This is used by {{ inline }}. """ self._template_path = template_path + self.parser = parser + self.templateEncoding = encoding or (self.parser.templateEncoding if self.parser else 'utf-8') try: self._file_name = os.path.abspath(template_path) self._file_mtime = os.path.getmtime(self._file_name) - with open(self._file_name) as templatefile: + # self.parser can be None in which case we default to utf-8 + with open(self._file_name, encoding=self.templateEncoding) as templatefile: raw_template = templatefile.read() - super(FileTemplate, self).__init__(raw_template, parser=parser) + super().__init__(raw_template, parser=parser) except (IOError, OSError): raise TemplateReadError('Cannot open: %r' % template_path) @@ -586,7 +604,7 @@ def ReloadIfModified(self): try: mtime = os.path.getmtime(self._file_name) if mtime > self._file_mtime: - with open(self._file_name) as templatefile: + with open(self._file_name, encoding=self.templateEncoding) as templatefile: template = templatefile.read() del self[:] self.scopes = [self] @@ -753,7 +771,7 @@ def __init__(self, tag, aliases): except TemplateSyntaxError: raise TemplateSyntaxError('Tag %r in {{ for }} loop is not valid' % tag) - super(TemplateLoop, self).__init__() + super().__init__() self.aliases = ''.join(aliases).split(',') self.aliascount = len(self.aliases) self.tag = tag From 6d4f216f020b73a9bab9b808c59cc45982f50a95 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 15 Feb 2021 10:09:15 +0100 Subject: [PATCH 080/118] update readme to reflect availability of pip package. --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8cab0bad..cd896bf6 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,9 @@ The following example applications for uWeb3 exist: # µWeb3 installation -The easiest and quickest way to install µWeb3 is using Python's `virtualenv`. Install using the setuptools installation script, which will automatically gather dependencies. +The easiest and quickest way to install µWeb3 is by running pip3 install uwebthree. + +For a development version using Python's `virtualenv`. Install using the setuptools installation script, which will automatically gather dependencies. ```bash # Set up the Python3 virtualenv From e407e411bedc88d0ec626dbd4ecebef42146599d Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 15 Feb 2021 10:22:15 +0100 Subject: [PATCH 081/118] tiny fix to set content on reponse directly, and set character encoding to utf-8 instead of the invalid utf8 --- uweb3/response.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/uweb3/response.py b/uweb3/response.py index 8323a12f..a86e3a47 100644 --- a/uweb3/response.py +++ b/uweb3/response.py @@ -35,9 +35,8 @@ def __init__(self, content='', content_type=CONTENT_TYPE, % headers: dict ~~ None A dictionary with header names and their associated values. """ - self.charset = kwds.get('charset', 'utf8') - self.content = None - self.text = content + self.charset = kwds.get('charset', 'utf-8') + self.content = content self.httpcode = httpcode self.headers = headers or {} if (content_type.startswith('text/') or From a53fabec1943841c1672f97f7da10607eba4d3d1 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 24 Feb 2021 12:08:48 +0100 Subject: [PATCH 082/118] Fix setup.py to be more lint friendly --- setup.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/setup.py b/setup.py index 5a89e3ea..d2182abb 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,4 @@ +#!/usr/bin/python3 """uWeb3 installer.""" import os @@ -5,17 +6,17 @@ from setuptools import setup, find_packages REQUIREMENTS = [ - 'PyMySQL', - 'pytz' + 'PyMySQL', + 'pytz' ] -def description(): +def Description(): """Returns the contents of the README.md file as description information.""" with open(os.path.join(os.path.dirname(__file__), 'README.md')) as r_file: return r_file.read() -def version(): +def Version(): """Returns the version of the library as read from the __init__.py file""" main_lib = os.path.join(os.path.dirname(__file__), 'uweb3', '__init__.py') with open(main_lib) as v_file: @@ -24,9 +25,9 @@ def version(): setup( name='uWebthree', - version=version(), + version=Version(), description='uWeb, python3, uswgi compatible micro web platform', - long_description=description(), + long_description=Description(), long_description_content_type='text/markdown', license='ISC', classifiers=[ From 5f03b2a526fee2e63d77f2c504cf67bc7c3eb4c3 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 24 Feb 2021 12:09:44 +0100 Subject: [PATCH 083/118] some cleanups, and fix an issue where an unknown content-type did not have any encoder and crashes the output --- uweb3/__init__.py | 26 ++++++++++---------------- 1 file changed, 10 insertions(+), 16 deletions(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 87421a91..0e81f17d 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -178,7 +178,8 @@ def __init__(self, page_class, routes, executing_path=None, config='config'): self.encoders = { 'text/html': lambda x: HTMLsafestring(x, unsafe=True), 'text/plain': str, - 'application/json': lambda x: JSONsafestring(x, unsafe=True)} + 'application/json': lambda x: JSONsafestring(x, unsafe=True), + 'default': str,} def __call__(self, env, start_response): """WSGI request handler. @@ -233,10 +234,9 @@ def __call__(self, env, start_response): response = req.response if not isinstance(response.text, Basesafestring): - # make sure we always output Safe HTML if our content type is something we should encode - encoder = self.encoders.get(response.clean_content_type(), None) - if encoder: - response.text = encoder(response.text) + # make sure we always output Safe Strings for our known content-types + encoder = self.encoders.get(response.clean_content_type(), self.encoders['default']) + response.text = encoder(response.text) if hasattr(pagemaker_instance, '_PostRequest'): pagemaker_instance._PostRequest() @@ -273,9 +273,8 @@ def _logging(self, req, response): """Logs incoming requests to a logfile. This is enabled by default, even if its missing in the config file. """ - if (self.config.options.get('development', None) - and self.config.options['development'].get('access_logging', - True) == 'False'): + if (self.config.options.get('development', None) and + self.config.options['development'].get('access_logging', True) == 'False'): return host = req.env['HTTP_HOST'].split(':')[0] @@ -304,9 +303,8 @@ def get_response(self, page_maker, method, args): except ImmediateResponse as err: return err[0] except Exception: - if (self.config.options.get('development', False) - and self.config.options['development'].get('error_logging', - True) == 'True'): + if (self.config.options.get('development', False) and + self.config.options['development'].get('error_logging', False) == 'True'): logger = logging.getLogger('uweb3_exception_logger') fh = logging.FileHandler(os.path.join(self.executing_path, 'uweb3_uncaught_exceptions.log')) logger.addHandler(fh) @@ -327,7 +325,6 @@ def serve(self): host = self.config.options['development'].get('host', host) port = self.config.options['development'].get('port', port) hotreload = self.config.options['development'].get('reload', False) in ('True', 'true') - dev = self.config.options['development'].get('dev', False) in ('True', 'true') interval = int(self.config.options['development'].get('checkinterval', 0)) ignored_extensions = self.config.options['development'].get('ignored_extensions', '').split(',') ignored_directories += self.config.options['development'].get('ignored_directories', '').split(',') @@ -366,7 +363,7 @@ class HotReload: execution path and restarts the server if needed""" IGNOREDEXTENSIONS = [".pyc", '.ini', '.md', '.html', '.log', '.sql'] - def __init__(self, path, interval=None, dev=False, ignored_extensions=None, ignored_directories=None): + def __init__(self, path, interval=None, ignored_extensions=None, ignored_directories=None): """Takes a path, an optional interval in seconds and an optional flag signaling a development environment which will set the path for new and changed file checking on the parent folder of the serving file.""" @@ -376,9 +373,6 @@ def __init__(self, path, interval=None, dev=False, ignored_extensions=None, igno self.path = os.path.dirname(path) self.ignoredextensions = self.IGNOREDEXTENSIONS + (ignored_extensions or []) self.ignoreddirectories = ignored_directories - if dev: - from pathlib import Path - self.path = str(Path(self.path).parents[1]) self.thread = threading.Thread(target=self.Run) self.thread.daemon = True self.thread.start() From 663c5e3cc2bed2789aa0de2529610def4f0087fb Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 24 Feb 2021 12:23:03 +0100 Subject: [PATCH 084/118] split out the various connectors to seperate files. Make sure the request object is found by scanning the stack, and this is only done when needed. --- uweb3/connections.py | 283 +++++++++---------------------- uweb3/connectors/Mongo.py | 30 ++++ uweb3/connectors/Mysql.py | 43 +++++ uweb3/connectors/SignedCookie.py | 34 ++++ uweb3/connectors/SqlAlchemy.py | 39 +++++ uweb3/connectors/Sqlite.py | 32 ++++ uweb3/connectors/__init__.py | 45 +++++ 7 files changed, 305 insertions(+), 201 deletions(-) create mode 100644 uweb3/connectors/Mongo.py create mode 100644 uweb3/connectors/Mysql.py create mode 100644 uweb3/connectors/SignedCookie.py create mode 100644 uweb3/connectors/SqlAlchemy.py create mode 100644 uweb3/connectors/Sqlite.py create mode 100644 uweb3/connectors/__init__.py diff --git a/uweb3/connections.py b/uweb3/connections.py index c28dcb43..696db444 100644 --- a/uweb3/connections.py +++ b/uweb3/connections.py @@ -1,18 +1,22 @@ -#!/usr/bin/python +#!/usr/bin/python3 """This file contains all the connectionManager classes that interact with databases, restfull apis, secure cookies, config files etc.""" __author__ = 'Jan Klopper (janunderdark.nl)' -__version__ = 0.1 +__version__ = 0.2 import os import sys from base64 import b64encode +import uweb3 + +from .connectors import * + class ConnectionError(Exception): """Error class thrown when the underlying connectors thrown an error on connecting.""" -class ConnectionManager: +class ConnectionManager(object): """This is the connection manager object that is handled by all Model Objects. It finds out which connection was requested by looking at the call stack, and figuring out what database type the model class calling it belongs to. @@ -24,26 +28,52 @@ class ConnectionManager: DEFAULTCONNECTIONMANAGER = None - def __init__(self, config, options, debug): + def __init__(self, config, options, debug, requestdepth=3, requestmaxdept=100): + """Initializes the ConnectionManager + + Arguments: + % config: reference to config parser + config instance + % options: reference to config parsers dict + options for the various settings provided in the config + % debug: bool, Optional defaults to False + Outputs extra debugging information if set to True + % requestdept: int, indicates how many stack layers we should start at + lookin up the stack to find our request object. Optional defaults to 2. + % requestdept: int, requestmaxdepth indicates how many stack layers at + maximum we should start at lookin up the stack to find our request object. + Optional defaults to 100. + + """ self.__connectors = {} # classes self.__connections = {} # instances self.config = config self.options = options self.debug = debug self.LoadDefaultConnectors() + self.requestdepth = requestdepth + self.requestmaxdepth = requestmaxdept def LoadDefaultConnectors(self): + """Populates the list of Connectors with the default available connectors""" self.RegisterConnector(SignedCookie) self.RegisterConnector(Mysql, True) self.RegisterConnector(Sqlite) self.RegisterConnector(Mongo) self.RegisterConnector(SqlAlchemy) - def RegisterConnector(self, classname, default=False): - """Make the ConnectonManager aware of a new type of connector.""" + def RegisterConnector(self, handler, default=False): + """Make the ConnectonManager aware of a new type of connector. + + Arguments: + % handler: class + Reference to the class that will handle the connections + % default: bool, Optional defaults to False + Should this Connector be considers the default connector? + """ if default: - self.DEFAULTCONNECTIONMANAGER = classname.Name() - self.__connectors[classname.Name()] = classname + self.DEFAULTCONNECTIONMANAGER = handler.Name() + self.__connectors[handler.Name()] = handler def RelevantConnection(self, level=2): """Returns the relevant database connection dependant on the caller model @@ -52,32 +82,64 @@ def RelevantConnection(self, level=2): If the caller model cannot be determined, the 'relational' database connection is returned as a fallback method. - Level indicates how many stack layers we should go up. Defaults to two. + Level indicates how many stack layers we should go up to find the current + caller_class which indicates our connector type. Defaults to 2. + + When no connection can be found or made due to a missing request from this + context a TypeError will be raised. + + When no connection can be found or made Due to a missing connector class a + TypeError will be raised. """ # Figure out caller type or instance # pylint: disable=W0212 #TODO use inspect module instead, and iterate over frames caller_locals = sys._getframe(level).f_locals # pylint: enable=W0212 + # Caller might be a Class or Class instance if 'self' in caller_locals: caller_cls = type(caller_locals['self']) else: caller_cls = caller_locals.get('cls', type) - # Decide the type of connection to return for this caller con_type = (caller_cls._CONNECTOR if hasattr(caller_cls, '_CONNECTOR') else self.DEFAULTCONNECTIONMANAGER) if (con_type in self.__connections and hasattr(self.__connections[con_type], 'connection')): return self.__connections[con_type].connection - request = sys._getframe(3).f_locals['self'].req + try: # instantiate a connection self.__connections[con_type] = self.__connectors[con_type]( - self.config, self.options, request, self.debug) + self.config, self.options, self.request, self.debug) return self.__connections[con_type].connection except KeyError as error: - raise TypeError('No connector for: %r, available: %r, %r' % (con_type, self.__connectors, error)) + raise TypeError('No connector for: %r, available: %r, %r' % ( + con_type, self.__connectors, error)) + + @property + def request(self): + """Returns the request object as looked up in the stack. + + When no connection can be found or made due to a missing request from this + context a TypeError will be raised. + + When no connection can be found or made Due to a missing connector class a + TypeError will be raised. + """ + requestdepth = self.requestdepth + while requestdepth < self.requestmaxdepth: + try: + parent = sys._getframe(requestdepth).f_locals['self'] + if isinstance(parent, uweb3.PageMaker) and hasattr(parent, 'req'): + request = parent.req + if self.debug: + print('request object found at stack level %d' % requestdepth) + return request + except (KeyError, AttributeError, ValueError): + pass + requestdepth = requestdepth + 1 + raise TypeError('No request could be found in call Stack.') def __enter__(self): """Proxies the transaction to the underlying relevant connection.""" @@ -91,7 +153,7 @@ def __getattr__(self, attribute): return getattr(self.RelevantConnection(), attribute) def RollbackAll(self): - """Performas a rollback on all connectors with pending commits""" + """Performs a rollback on all connectors with pending commits.""" if self.debug: print('Rolling back uncommited transaction on all connectors.') for classname in self.__connections: @@ -101,7 +163,10 @@ def RollbackAll(self): pass def PostRequest(self): - """This cleans up any non persistent connections.""" + """This cleans up any non persistent connections. + Eg, connections that rely on request information, or connections that should + not be kept alive beyond the scope of a request. + """ cleanups = [ classname for classname in self.__connections if (hasattr(self.__connections[classname], 'PERSISTENT') @@ -122,7 +187,8 @@ def __iter__(self): def __del__(self): """Cleans up all references, and closes all connectors""" - print('Deleting model connections.') + if self.debug: + print('Deleting model connections.') for classname in self.__connectors: if not hasattr(self.__connectors[classname], 'connection'): continue @@ -130,188 +196,3 @@ def __del__(self): self.__connections[classname].Disconnect() except (NotImplementedError, TypeError, ConnectionError): pass - - -class Connector: - """Base Connector class, subclass from this to create your own connectors. - Usually the name of your class is used to lookup its config in the - configuration file, or the database or local filename. - - Connectors based on this class are Usually Singletons. One global connection - is kept alive, and multiple model classes use it to connect to their - respective tables, cookies, or files. - """ - _NAME = None - - @classmethod - def Name(cls): - """Returns the 'connector' name, which is usally used to lookup its config - in the config file. - - If this is not explicitly defined by the class constant `_TABLE`, the return - value will be the class name with the first letter lowercased. - """ - if cls._NAME: - return cls._NAME - name = cls.__name__ - return name[0].lower() + name[1:] - - def Disconnect(self): - """Standard interface to disconnect from data source""" - raise NotImplementedError - - def Rollback(self): - """Standard interface to rollback any pending commits""" - raise NotImplementedError - - -class SignedCookie(Connector): - """Adds a signed cookie connection to the connection manager object. - - The name of the class is used as the Cookiename""" - - PERSISTENT = False - - def __init__(self, config, options, request, debug=False): - """Sets up the local connection to the signed cookie store, and generates a - new secret key if no key can be found in the config""" - # Generating random seeds on uWeb3 startup or fetch from config - self.debug = debug - try: - self.options = options[self.Name()] - self.secure_cookie_secret = self.options['secret'] - except KeyError: - secret = self.GenerateNewKey() - config.Create(self.Name(), 'secret', secret) - if self.debug: - print('SignedCookie: Wrote new secret random to config.') - self.secure_cookie_secret = secret - self.connection = (request, request.vars['cookie'], self.secure_cookie_secret) - - @staticmethod - def GenerateNewKey(length=128): - return b64encode(os.urandom(length)).decode('utf-8') - - -class Mysql(Connector): - """Adds MySQL support to connection manager object.""" - - def __init__(self, config, options, request, debug=False): - """Returns a MySQL database connection.""" - self.debug = debug - self.options = {'host': 'localhost', - 'user': None, - 'password': None, - 'database': ''} - try: - from .libs.sqltalk import mysql - try: - self.options = options[self.Name()] - except KeyError: - pass - self.connection = mysql.Connect( - host=self.options.get('host', 'localhost'), - user=self.options.get('user'), - passwd=self.options.get('password'), - db=self.options.get('database'), - charset=self.options.get('charset', 'utf8'), - debug=self.debug) - except Exception as e: - raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) - - def Rollback(self): - with self.connection as cursor: - return cursor.Execute("ROLLBACK") - - def Disconnect(self): - """Closes the MySQL connection.""" - if self.debug: - print('%s closed connection to: %r' % (self.Name(), self.options.get('database'))) - self.connection.close() - del(self.connection) - - -class Mongo(Connector): - """Adds MongoDB support to connection manager object.""" - - def __init__(self, config, options, request, debug=False): - """Returns a MongoDB database connection.""" - self.debug = debug - import pymongo - self.options = options.get(self.Name(), {}) - try: - self.connection = pymongo.connection.Connection( - host=self.options.get('host', 'localhost'), - port=self.options.get('port', 27017)) - if 'database' in self.options: - self.connection = self.connection[self.options['database']] - except Exception as e: - raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) - - def Disconnect(self): - """Closes the Mongo connection.""" - if self.debug: - print('%s closed connection to: %r' % (self.Name(), self.options.get('database', 'Unspecified'))) - self.connection.close() - del(self.connection) - - -class SqlAlchemy(Connector): - """Adds MysqlAlchemy connection to ConnectionManager.""" - - def __init__(self, config, options, request, debug=False): - """Returns a Mysql database connection wrapped in a SQLAlchemy session.""" - from sqlalchemy.orm import sessionmaker - self.debug = debug - self.options = {'host': 'localhost', - 'user': None, - 'password': None, - 'database': ''} - try: - self.options = options[self.Name()] - except KeyError: - pass - Session = sessionmaker() - Session.configure(bind=self.engine, expire_on_commit=False) - try: - self.connection = Session() - except Exception as e: - raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) - - def engine(self): - from sqlalchemy import create_engine - return create_engine('mysql://{username}:{password}@{host}/{database}'.format( - username=self.options.get('user'), - password=self.options.get('password'), - host=self.options.get('host', 'localhost'), - database=self.options.get('database')), - pool_size=5, - max_overflow=0, - encoding=self.options.get('charset', 'utf8'),) - - -class Sqlite(Connector): - """Adds SQLite support to connection manager object.""" - - def __init__(self, config, options, request, debug=False): - """Returns a SQLite database connection. - The name of the class is used as the local filename. - """ - from .libs.sqltalk import sqlite - self.debug = debug - self.options = options[self.Name()] - try: - self.connection = sqlite.Connect(self.options.get('database')) - except Exception as e: - raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) - - def Rollback(self): - """Rolls back any uncommited transactions.""" - return self.connection.rollback() - - def Disconnect(self): - """Closes the SQLite connection.""" - if self.debug: - print('%s closed connection to: %r' % (self.Name(), self.options.get('database'))) - self.connection.close() - del(self.connection) diff --git a/uweb3/connectors/Mongo.py b/uweb3/connectors/Mongo.py new file mode 100644 index 00000000..bc68480c --- /dev/null +++ b/uweb3/connectors/Mongo.py @@ -0,0 +1,30 @@ +#!/usr/bin/python3 +"""This file contains the connector for Mongo.""" +__author__ = 'Jan Klopper (janunderdark.nl)' +__version__ = 0.1 + +from . import Connector + +class Mongo(Connector): + """Adds MongoDB support to connection manager object.""" + + def __init__(self, config, options, request, debug=False): + """Returns a MongoDB database connection.""" + self.debug = debug + import pymongo + self.options = options.get(self.Name(), {}) + try: + self.connection = pymongo.connection.Connection( + host=self.options.get('host', 'localhost'), + port=self.options.get('port', 27017)) + if 'database' in self.options: + self.connection = self.connection[self.options['database']] + except Exception as e: + raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) + + def Disconnect(self): + """Closes the Mongo connection.""" + if self.debug: + print('%s closed connection to: %r' % (self.Name(), self.options.get('database', 'Unspecified'))) + self.connection.close() + del(self.connection) diff --git a/uweb3/connectors/Mysql.py b/uweb3/connectors/Mysql.py new file mode 100644 index 00000000..77dad345 --- /dev/null +++ b/uweb3/connectors/Mysql.py @@ -0,0 +1,43 @@ +#!/usr/bin/python3 +"""This file contains the connector for Mysql.""" +__author__ = 'Jan Klopper (janunderdark.nl)' +__version__ = 0.1 + +from . import Connector + +class Mysql(Connector): + """Adds MySQL support to connection manager object.""" + + def __init__(self, config, options, request, debug=False): + """Returns a MySQL database connection.""" + self.debug = debug + self.options = {'host': 'localhost', + 'user': None, + 'password': None, + 'database': ''} + try: + from ..libs.sqltalk import mysql + try: + self.options = options[self.Name()] + except KeyError: + pass + self.connection = mysql.Connect( + host=self.options.get('host', 'localhost'), + user=self.options.get('user'), + passwd=self.options.get('password'), + db=self.options.get('database'), + charset=self.options.get('charset', 'utf8'), + debug=self.debug) + except Exception as e: + raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) + + def Rollback(self): + with self.connection as cursor: + return cursor.Execute("ROLLBACK") + + def Disconnect(self): + """Closes the MySQL connection.""" + if self.debug: + print('%s closed connection to: %r' % (self.Name(), self.options.get('database'))) + self.connection.close() + del(self.connection) diff --git a/uweb3/connectors/SignedCookie.py b/uweb3/connectors/SignedCookie.py new file mode 100644 index 00000000..d9ad4467 --- /dev/null +++ b/uweb3/connectors/SignedCookie.py @@ -0,0 +1,34 @@ +#!/usr/bin/python3 +"""This file contains the connector for Signed cookies.""" +__author__ = 'Jan Klopper (janunderdark.nl)' +__version__ = 0.1 + +from . import Connector + +class SignedCookie(Connector): + """Adds a signed cookie connection to the connection manager object. + + The name of the class is used as the Cookiename""" + + PERSISTENT = False + + def __init__(self, config, options, request, debug=False): + """Sets up the local connection to the signed cookie store, and generates a + new secret key if no key can be found in the config""" + self.debug = debug + # Generating random seeds on uWeb3 startup or fetch from config + try: + self.options = options[self.Name()] + self.secure_cookie_secret = self.options['secret'] + except KeyError: + secret = self.GenerateNewKey() + config.Create(self.Name(), 'secret', secret) + if self.debug: + print('SignedCookie: Wrote new secret random to config.') + self.secure_cookie_secret = secret + print('SignedCookie INIT', request.vars) + self.connection = (request, request.vars['cookie'], self.secure_cookie_secret) + + @staticmethod + def GenerateNewKey(length=128): + return b64encode(os.urandom(length)).decode('utf-8') diff --git a/uweb3/connectors/SqlAlchemy.py b/uweb3/connectors/SqlAlchemy.py new file mode 100644 index 00000000..b934a29c --- /dev/null +++ b/uweb3/connectors/SqlAlchemy.py @@ -0,0 +1,39 @@ +#!/usr/bin/python3 +"""This file contains the connector for SqlAlchemy.""" +__author__ = 'Jan Klopper (janunderdark.nl)' +__version__ = 0.1 + +from . import Connector + +class SqlAlchemy(Connector): + """Adds MysqlAlchemy connection to ConnectionManager.""" + + def __init__(self, config, options, request, debug=False): + """Returns a Mysql database connection wrapped in a SQLAlchemy session.""" + from sqlalchemy.orm import sessionmaker + self.debug = debug + self.options = {'host': 'localhost', + 'user': None, + 'password': None, + 'database': ''} + try: + self.options = options[self.Name()] + except KeyError: + pass + Session = sessionmaker() + Session.configure(bind=self.engine, expire_on_commit=False) + try: + self.connection = Session() + except Exception as e: + raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) + + def engine(self): + from sqlalchemy import create_engine + return create_engine('mysql://{username}:{password}@{host}/{database}'.format( + username=self.options.get('user'), + password=self.options.get('password'), + host=self.options.get('host', 'localhost'), + database=self.options.get('database')), + pool_size=5, + max_overflow=0, + encoding=self.options.get('charset', 'utf8'),) diff --git a/uweb3/connectors/Sqlite.py b/uweb3/connectors/Sqlite.py new file mode 100644 index 00000000..9e9780d1 --- /dev/null +++ b/uweb3/connectors/Sqlite.py @@ -0,0 +1,32 @@ +#!/usr/bin/python3 +"""This file contains the connector for Sqlite.""" +__author__ = 'Jan Klopper (janunderdark.nl)' +__version__ = 0.1 + +from . import Connector + +class Sqlite(Connector): + """Adds SQLite support to connection manager object.""" + + def __init__(self, config, options, request, debug=False): + """Returns a SQLite database connection. + The name of the class is used as the local filename. + """ + from ..libs.sqltalk import sqlite + self.debug = debug + self.options = options[self.Name()] + try: + self.connection = sqlite.Connect(self.options.get('database')) + except Exception as e: + raise ConnectionError('Connection to "%s" of type "%s" resulted in: %r' % (self.Name(), type(self), e)) + + def Rollback(self): + """Rolls back any uncommited transactions.""" + return self.connection.rollback() + + def Disconnect(self): + """Closes the SQLite connection.""" + if self.debug: + print('%s closed connection to: %r' % (self.Name(), self.options.get('database'))) + self.connection.close() + del(self.connection) diff --git a/uweb3/connectors/__init__.py b/uweb3/connectors/__init__.py new file mode 100644 index 00000000..8725a577 --- /dev/null +++ b/uweb3/connectors/__init__.py @@ -0,0 +1,45 @@ +#!/usr/bin/python3 +"""This file contains the Base connector for model connections and imports all +available connectors.""" + +__author__ = 'Jan Klopper (janunderdark.nl)' +__version__ = 0.1 + +class Connector(object): + """Base Connector class, subclass from this to create your own connectors. + Usually the name of your class is used to lookup its config in the + configuration file, or the database or local filename. + + Connectors based on this class are Usually Singletons. One global connection + is kept alive, and multiple model classes use it to connect to their + respective tables, cookies, or files. + """ + _NAME = None + + @classmethod + def Name(cls): + """Returns the 'connector' name, which is usally used to lookup its config + in the config file. + + If this is not explicitly defined by the class constant `_TABLE`, the return + value will be the class name with the first letter lowercased. + """ + if cls._NAME: + return cls._NAME + name = cls.__name__ + return name[0].lower() + name[1:] + + def Disconnect(self): + """Standard interface to disconnect from data source""" + raise NotImplementedError + + def Rollback(self): + """Standard interface to rollback any pending commits""" + raise NotImplementedError + +from .SignedCookie import SignedCookie +from .Mysql import Mysql +from .Mongo import Mongo +from .Sqlite import Sqlite +from .SqlAlchemy import SqlAlchemy +from .Restfulljson import Restfulljson From 0bc3973e13a094d178b7e758a80cb486d410af26 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 24 Feb 2021 12:23:24 +0100 Subject: [PATCH 085/118] add pylintrc file --- pylintrc | 311 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 311 insertions(+) create mode 100644 pylintrc diff --git a/pylintrc b/pylintrc new file mode 100644 index 00000000..03d26900 --- /dev/null +++ b/pylintrc @@ -0,0 +1,311 @@ +# lint Python modules using external checkers. +# +# This is the main checker controling the other ones and the reports +# generation. It is itself both a raw checker and an astng checker in order +# to: +# * handle message activation / deactivation at the module level +# * handle some basic but necessary stats'data (number of classes, methods...) +# +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add to the black list. It should be a base name, not a +# path. You may set this option multiple times. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# Set the cache size for astng objects. +cache-size=500 + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable only checker(s) with the given id(s). This option conflicts with the +# disable-checker option +#enable-checker= + +# Enable all checker(s) except those with the given id(s). This option +# conflicts with the enable-checker option +#disable-checker= + +# Enable all messages in the listed categories. +#enable-msg-cat= + +# Disable all messages in the listed categories. +#disable-msg-cat= + +# Enable the message(s) with the given id(s). +#enable-msg= + +# Disable the message(s) with the given id(s). +# W0142: Used * or ** magic -- this is in fact a very Pythonic approach. +# W0403: Relative import %r -- Underdark tree structure demands this. +# E1103: %s %r has no %r member (but some types could not be inferred) +# This is generally not an actual problem, and causes false positives +disable-msg=W0403, W0142, E1103 +disable=W0403, W0142, E1103 + + +[REPORTS] + +# set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html +output-format=colorized + +# Include message's id in output +include-ids=yes + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells wether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note).You have access to the variables errors warning, statement which +# respectivly contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (R0004). +evaluation=10.0 - (5.0 * error + warning + refactor + convention) / statement * 10 + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (R0004). +comment=yes + +# Enable the report(s) with the given id(s). +#enable-report= + +# Disable the report(s) with the given id(s). +# R0801 is the "similar lines" report, which is not used. +disable-report=R0801 + + +# checks for +# * unused variables / imports +# * undefined variables +# * redefinition of variable from builtins or from an outer scope +# * use of variable before assigment +# +[VARIABLES] + +# Tells wether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching names used for dummy variables (i.e. not used). +dummy-variables-rgx=_.*$ + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +# try to find bugs in the code using type inference +# +[TYPECHECK] + +# Tells wether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# When zope mode is activated, consider the acquired-members option to ignore +# access to some undefined attributes. +zope=no + +# List of members which are usually get through zope's acquisition mecanism and +# so shouldn't trigger E0201 when accessed (need zope=yes to be considered). +acquired-members=REQUEST,acl_users,aq_parent + + +# checks for : +# * doc strings +# * modules / classes / functions / methods / arguments / variables name +# * number of arguments, local variables, branchs, returns and statements in +# functions, methods +# * required module attributes +# * dangerous default values as arguments +# * redefinition of function / method / class +# * uses of the global statement +# +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes=__author__, __version__ + +# Regular expression which should only match functions or classes name which do +# not require a docstring +no-docstring-rgx=_.*$ + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=([A-Z_][a-zA-Z0-9_]{2,30})|(main)$ + +# Regular expression which should only match correct method names +method-rgx=([a-zA-Z_][a-zA-Z0-9_]{2,30})|(test.*)$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,n + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar + +# List of builtins function names that should not be used, separated by a comma +bad-functions=filter,apply,input + + +# checks for +# * external modules dependencies +# * relative / wildcard imports +# * cyclic imports +# * uses of deprecated modules +# +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,string,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report R0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report R0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report R0402 must +# not be disabled) +int-import-graph= + + +# checks for : +# * methods without self as first argument +# * overridden methods signature +# * access only to existant members via self +# * attributes not defined in the __init__ method +# * supported interfaces implementation +# * unreachable code +# +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods= + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp,run + + +# checks for sign of poor/misdesign: +# * number of methods, attributes, local variables... +# * size, complexity of functions, methods +# +[DESIGN] + +# Maximum number of arguments for function / method +max-args=8 + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branchs=20 + +# Maximum number of statements in function / method body +max-statements=40 + +# Maximum number of parents for a class (see R0901). +max-parents=12 + +# Maximum number of attributes for a class (see R0902). +max-attributes=10 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=0 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +# checks for: +# * warning notes in the code like FIXME, XXX +# * PEP 263: source code with non ascii character but no encoding declaration +# +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +# checks for : +# * unauthorized constructions +# * strict indentation +# * line length +# * use of <> instead of != +# +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +# checks for similarities and duplicated code. This computation may be +# memory / CPU intensive, so you should disable it if you experiments some +# problems. +# +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes From 13af2584223775df744387475e8c306d84414c65 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 24 Feb 2021 12:23:58 +0100 Subject: [PATCH 086/118] don't include restfull api connector just yet --- uweb3/connectors/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/uweb3/connectors/__init__.py b/uweb3/connectors/__init__.py index 8725a577..16995f0c 100644 --- a/uweb3/connectors/__init__.py +++ b/uweb3/connectors/__init__.py @@ -2,7 +2,7 @@ """This file contains the Base connector for model connections and imports all available connectors.""" -__author__ = 'Jan Klopper (janunderdark.nl)' +__author__ = 'Jan Klopper (jan@underdark.nl)' __version__ = 0.1 class Connector(object): @@ -42,4 +42,3 @@ def Rollback(self): from .Mongo import Mongo from .Sqlite import Sqlite from .SqlAlchemy import SqlAlchemy -from .Restfulljson import Restfulljson From 830feee0828ffcb19b00682a2d18175c99a69a0e Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 24 Feb 2021 12:28:13 +0100 Subject: [PATCH 087/118] various fixes set file to execute using python3, and use new style classes --- uweb3/libs/sqltalk/mysql/connection.py | 2 +- uweb3/libs/sqltalk/sqlresult.py | 6 +++--- uweb3/libs/sqltalk/sqlresult_test.py | 2 +- uweb3/templateparser.py | 8 ++++---- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/uweb3/libs/sqltalk/mysql/connection.py b/uweb3/libs/sqltalk/mysql/connection.py index d5140e25..c6520bb0 100644 --- a/uweb3/libs/sqltalk/mysql/connection.py +++ b/uweb3/libs/sqltalk/mysql/connection.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python3 """This module implements the Connection class, which sets up a connection to a MySQL database. From this connection, cursor objects can be created, which use the escaping and character encoding facilities offered by the connection. diff --git a/uweb3/libs/sqltalk/sqlresult.py b/uweb3/libs/sqltalk/sqlresult.py index a4913d78..165c2821 100644 --- a/uweb3/libs/sqltalk/sqlresult.py +++ b/uweb3/libs/sqltalk/sqlresult.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python3 """SQL result abstraction module. Classes: @@ -32,7 +32,7 @@ class NotSupportedError(Error, TypeError): """Operation is not supported.""" -class ResultRow: +class ResultRow(object): """SQL Result row - an ordered dictionary-like record abstraction. ResultRow has two item retrieval interfaces: @@ -168,7 +168,7 @@ def popitem(self): return self._fields.pop(), self._values.pop() -class ResultSet: +class ResultSet(object): """SQL Result set - stores the query, the returned result, and other info. ResultSet is created from immutable objects. Once defined, none of its diff --git a/uweb3/libs/sqltalk/sqlresult_test.py b/uweb3/libs/sqltalk/sqlresult_test.py index c584a771..12943823 100644 --- a/uweb3/libs/sqltalk/sqlresult_test.py +++ b/uweb3/libs/sqltalk/sqlresult_test.py @@ -1,4 +1,4 @@ -#!/usr/bin/python2.5 +#!/usr/bin/python3 """Testsuite for the SQL Result abstraction module.""" __author__ = 'Elmer de Looff ' __version__ = '0.6' diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index 802ea96c..8fb753b5 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 """uWeb TemplateParser Classes: @@ -616,7 +616,7 @@ def ReloadIfModified(self): pass -class TemplateConditional: +class TemplateConditional(object): """A template construct to control flow based on the value of a tag.""" def __init__(self, expr, astvisitor): self.branches = [] @@ -808,7 +808,7 @@ def Parse(self, **kwds): return ''.join(output) -class TemplateTag: +class TemplateTag(object): """Template tags are used for dynamic placeholders in templates. Their final value is determined during parsing. For more explanation on this, @@ -1029,7 +1029,7 @@ def Parse(self, **_kwds): return str(self) -class JITTag: +class JITTag(object): """This is a template Tag which is only evaulated on replacement. It is usefull for situations where not all all of this functions input vars are available just yet. From 88770e375245f243957e94866847f18262beae86 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 24 Feb 2021 12:28:39 +0100 Subject: [PATCH 088/118] linint cleanups and some minor refactoring --- uweb3/sockets.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/uweb3/sockets.py b/uweb3/sockets.py index d6cc218c..fc4c0280 100644 --- a/uweb3/sockets.py +++ b/uweb3/sockets.py @@ -12,18 +12,20 @@ class SocketMiddleWare(socketio.WSGIApp): def __init__(self, socketio_server, uweb3_server, socketio_path='socket.io'): super(SocketMiddleWare, self).__init__(socketio_server, uweb3_server, - socketio_path=socketio_path - ) + socketio_path=socketio_path) class Uweb3SocketIO: - def __init__(self, app, sio, static_dir=os.path.dirname(os.path.abspath(__file__))): + def __init__(self, app, sio, static_dir=None): if not isinstance(app, uWeb): raise Exception("App must be an uWeb3 instance!") + if not static_dir: + static_dir = os.path.dirname(os.path.abspath(__file__)) self.host = app.config.options['development'].get('host', '127.0.0.1') self.port = app.config.options['development'].get('port', 8000) if app.config.options['development'].get('dev', False) == 'True': - HotReload(app.executing_path, uweb_dev=app.config.options['development'].get('uweb_dev', 'False')) + hotreload = app.config.options['development'].get('uweb_dev', 'False') + HotReload(app.executing_path, uweb_dev=hotreload) self.setup_app(app, sio, static_dir) @@ -31,5 +33,6 @@ def setup_app(self, app, sio, static_dir): path = os.path.join(app.executing_path, 'static') app = SocketMiddleWare(sio, app) static_directory = [os.path.join(sys.path[0], path)] - app = StaticMiddleware(app, static_root='static', static_dirs=static_directory) + app = StaticMiddleware(app, static_root='static', + static_dirs=static_directory) eventlet.wsgi.server(eventlet.listen((self.host, int(self.port))), app) From e61cb2adb8e0aa32b1f64cc23e783d472e542bf5 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 24 Feb 2021 12:29:10 +0100 Subject: [PATCH 089/118] dont crash when we dont have a content-type header set --- uweb3/request.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uweb3/request.py b/uweb3/request.py index fa1c227a..9247a419 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -74,7 +74,7 @@ def __init__(self, env, registry): pass request_payload = self.env['wsgi.input'].read(request_body_size) self.input = request_payload - if self.env['CONTENT_TYPE'] == 'application/json': + if self.env.get('CONTENT_TYPE', '') == 'application/json': self.vars[self.method.lower()] = json.loads(request_payload) else: self.vars[self.method.lower()] = IndexedFieldStorage(stringIO.StringIO(request_payload.decode("utf-8")), From 06a39dbc3fffa0f0840b7905d0098d7c7171d2d1 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 24 Feb 2021 12:29:45 +0100 Subject: [PATCH 090/118] linting fixes and some minor refactoring --- uweb3/response.py | 45 +++++++++++++++++++++++++++++++++------------ 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/uweb3/response.py b/uweb3/response.py index a86e3a47..9c4f52c4 100644 --- a/uweb3/response.py +++ b/uweb3/response.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 """uWeb3 response classes.""" # Standard modules @@ -7,11 +7,7 @@ except ImportError: import http.client as httplib -import json - -from collections import defaultdict - -class Response: +class Response(object): """Defines a full HTTP response. The full response consists of a required content part, and then optional @@ -29,7 +25,7 @@ def __init__(self, content='', content_type=CONTENT_TYPE, The content to return to the client. This can be either plain text, html or the contents of a file (images for example). % content_type: str ~~ CONTENT_TYPE ('text/html' by default) - The content type of the response. This should NOT be set in headers. + The Content-Type of the response. This should NOT be set in headers. % httpcode: int ~~ 200 The HTTP response code to attach to the response. % headers: dict ~~ None @@ -39,24 +35,34 @@ def __init__(self, content='', content_type=CONTENT_TYPE, self.content = content self.httpcode = httpcode self.headers = headers or {} - if (content_type.startswith('text/') or - content_type.startswith('application/json')) and ';' not in content_type: + if (';' not in content_type and + (content_type.startswith('text/') or + content_type.startswith('application/json'))): content_type = '{!s}; charset={!s}'.format(content_type, self.charset) self.content_type = content_type # Get and set content-type header @property def content_type(self): - return self.headers['Content-Type'] + """Returns the current Content-Type or None if not set""" + return self.headers.get('Content-Type', None) @content_type.setter def content_type(self, content_type): + """Sets the Content-Type of the response + + Arguments: + @ content_type: str ~~ CONTENT_TYPE + The content type of the response. + """ current = self.headers.get('Content-Type', '') if ';' in current: - content_type = '{!s}; {!s}'.format(content_type, current.split(';', 1)[-1]) + content_type = '{!s}; {!s}'.format(content_type, + current.split(';', 1)[-1]) self.headers['Content-Type'] = content_type def clean_content_type(self): + """Returns the Content-Type, cleaned from any characters set information.""" if ';' not in self.headers['Content-Type']: return self.headers['Content-Type'] return self.headers['Content-Type'].split(';')[0] @@ -64,20 +70,34 @@ def clean_content_type(self): # Get and set body text @property def text(self): + """Returns the content of this response""" return self.content @text.setter def text(self, content): + """Sets the content of this response. + + Arguments: + @ content: str + The content to return to the client. This can be either plain text, html + or the contents of a file (images for example). + """ self.content = content # Retrieve a header list @property def headerlist(self): + """Returns the current headers as a list of tuples + + each tuple contains the header key, and its value. + """ tuple_list = [] for key, val in self.headers.items(): if key == 'Set-Cookie': for cookie in val: - tuple_list.append((key, cookie.encode('ascii', 'ignore').decode('ascii'))) + tuple_list.append( + (key, cookie.encode('ascii', 'ignore').decode('ascii')) + ) continue if not isinstance(val, str): val = str(val) @@ -86,6 +106,7 @@ def headerlist(self): @property def status(self): + """Returns the current http status code for this response.""" if not self.httpcode: return '%d %s' % (500, httplib.responses[500]) return '%d %s' % (self.httpcode, httplib.responses[self.httpcode]) From e6df6d72e2530983701eb573947ac31249e05acd Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 24 Feb 2021 12:30:36 +0100 Subject: [PATCH 091/118] refactoring to the SecureCookie class and some minor refactoring + use new style classes --- uweb3/model.py | 39 ++++++++++++++++++++++----------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/uweb3/model.py b/uweb3/model.py index 7bc04490..433ab814 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -1,4 +1,4 @@ -#!/usr/bin/python +#!/usr/bin/python3 """uWeb3 model base classes.""" # Standard modules @@ -35,7 +35,7 @@ class PermissionError(Error): """The entity has insufficient rights to access the resource.""" -class SettingsManager: +class SettingsManager(object): def __init__(self, filename=None, path=None): """Creates a ini file with the child class name @@ -173,7 +173,7 @@ def _Write(self, reread=True): return True -class SecureCookie: +class SecureCookie(object): """The secureCookie class works just like other data abstraction classes, except that it stores its data in client side cookies that are signed with a server side secret to avoid tampering by the end-user. @@ -189,11 +189,11 @@ class SecureCookie: def __init__(self, connection): """Create a new SecureCookie instance.""" self.connection = connection - self.req, self.cookies, self.cookie_salt = self.connection - self.rawcookie = self.__GetCookie() + self.request, self.cookies, self.cookie_salt = self.connection self.debug = self.connection.debug + self._rawcookie = None if self.debug: - print(self.cookies) + print('current cookies (unvalidated) for request:', self.cookies) def __str__(self): """Returns the cookie's value if it was valid and untampered with.""" @@ -213,19 +213,24 @@ def TableName(cls): name = cls.__name__ return name[0].lower() + name[1:] - def __GetCookie(self): + @property + def rawcookie(self): """Reads the request cookie, checks if it was signed correctly and return the value, or returns False""" + if not self._rawcookie is None: + return self._rawcookie name = self.TableName() if name in self.cookies and self.cookies[name]: isValid, value = self.__ValidateCookieHash(self.cookies[name]) if isValid: + self._rawcookie = value return value if self.debug: - print('Secure cookie "%s" was tampered with and thus invalid.' % name) + print('Secure cookie "%s" was tampered with and thus invalid. content was: %s ' % (name, self.cookies[name])) if self.debug: print('Secure cookie "%s" was not present.' % name) - return '' + self._rawcookie = '' + return self._rawcookie @classmethod def Create(cls, connection, data, **attrs): @@ -266,13 +271,13 @@ def Create(cls, connection, data, **attrs): ValueError: When cookie with name already exists """ cls.connection = connection - cls.req, cls.cookies, cls.cookie_salt = connection + cls.request, cls.cookies, cls.cookie_salt = connection name = cls.TableName() - cls.rawcookie = data + cls._rawcookie = data hashed = cls.__CreateCookieHash(cls, data) cls.cookies[name] = hashed - cls.req.AddCookie(name, hashed, **attrs) + cls.request.AddCookie(name, hashed, **attrs) return cls def Update(self, data, **attrs): @@ -318,16 +323,16 @@ def Update(self, data, **attrs): name = self.TableName() if not self.rawcookie: raise ValueError("No valid cookie with name `{}` found".format(name)) - self.rawcookie = data - self.req.AddCookie(name, self.__CreateCookieHash(data), **attrs) + self._rawcookie = data + self.request.AddCookie(name, self.__CreateCookieHash(data), **attrs) def Delete(self): """Deletes cookie based on name The cookie is no longer in the session after calling this method """ name = self.TableName() - self.req.DeleteCookie(name) - self.rawcookie = None + self.request.DeleteCookie(name) + self._rawcookie = None def __CreateCookieHash(self, data): hex_string = pickle.dumps(data).hex() @@ -1611,7 +1616,7 @@ def GetSubTypes(cls, seen=None): import functools -class CachedPage: +class CachedPage(object): """Abstraction class for the cached Pages table in the database.""" MAXAGE = 61 From 8b4fa2f97bb518e8e43d4efe268e3af9cb01ed94 Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 26 Feb 2021 09:15:29 +0100 Subject: [PATCH 092/118] remove unneeded debug print --- uweb3/connectors/SignedCookie.py | 1 - 1 file changed, 1 deletion(-) diff --git a/uweb3/connectors/SignedCookie.py b/uweb3/connectors/SignedCookie.py index d9ad4467..a57b5758 100644 --- a/uweb3/connectors/SignedCookie.py +++ b/uweb3/connectors/SignedCookie.py @@ -26,7 +26,6 @@ def __init__(self, config, options, request, debug=False): if self.debug: print('SignedCookie: Wrote new secret random to config.') self.secure_cookie_secret = secret - print('SignedCookie INIT', request.vars) self.connection = (request, request.vars['cookie'], self.secure_cookie_secret) @staticmethod From 9334f9328d74013a30947da0a11f3ca9348b772f Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 26 Feb 2021 16:02:14 +0100 Subject: [PATCH 093/118] remove first slash from relative paths in the static handler --- uweb3/pagemaker/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 5dc89594..ec962928 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -382,7 +382,9 @@ def Static(self, rel_path): page if the file was not available on the local path. """ - abs_path = os.path.realpath(os.path.join(self.PUBLIC_DIR, rel_path)) + abs_path = os.path.realpath(os.path.join(self.PUBLIC_DIR, rel_path.lstrip('/'))) + print(abs_path, self.PUBLIC_DIR) + if os.path.commonprefix((abs_path, self.PUBLIC_DIR)) != self.PUBLIC_DIR: return self._StaticNotFound(rel_path) try: From 4f61f85b57d4b0ffc9b373851a74754e1ebc67c5 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 5 Apr 2021 10:49:03 +0200 Subject: [PATCH 094/118] fix module loading in Signedcookie connector --- uweb3/__init__.py | 2 +- uweb3/connections.py | 2 -- uweb3/connectors/SignedCookie.py | 3 +++ 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 0e81f17d..f24dedb3 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """µWeb3 Framework""" -__version__ = '3.0.1' +__version__ = '3.0.3' # Standard modules import configparser diff --git a/uweb3/connections.py b/uweb3/connections.py index 696db444..2f4d54d2 100644 --- a/uweb3/connections.py +++ b/uweb3/connections.py @@ -4,9 +4,7 @@ __author__ = 'Jan Klopper (janunderdark.nl)' __version__ = 0.2 -import os import sys -from base64 import b64encode import uweb3 diff --git a/uweb3/connectors/SignedCookie.py b/uweb3/connectors/SignedCookie.py index a57b5758..d4a1b1a7 100644 --- a/uweb3/connectors/SignedCookie.py +++ b/uweb3/connectors/SignedCookie.py @@ -3,6 +3,9 @@ __author__ = 'Jan Klopper (janunderdark.nl)' __version__ = 0.1 +import os +from base64 import b64encode + from . import Connector class SignedCookie(Connector): From fc31c24843923af21afd7bebcf408b46350d7f34 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 5 Apr 2021 10:53:21 +0200 Subject: [PATCH 095/118] fix hot reloading by not adding empty strings to ignored_extensions and ignored_directories --- uweb3/__init__.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index f24dedb3..f148e75e 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -2,7 +2,7 @@ # -*- coding: utf-8 -*- """µWeb3 Framework""" -__version__ = '3.0.3' +__version__ = '3.0.4' # Standard modules import configparser @@ -316,25 +316,27 @@ def serve(self): host = 'localhost' port = 8001 hotreload = False - dev = False interval = None ignored_directories = ['__pycache__', self.inital_pagemaker.PUBLIC_DIR, self.inital_pagemaker.TEMPLATE_DIR] + ignored_extensions = [] if self.config.options.get('development', False): host = self.config.options['development'].get('host', host) port = self.config.options['development'].get('port', port) hotreload = self.config.options['development'].get('reload', False) in ('True', 'true') interval = int(self.config.options['development'].get('checkinterval', 0)) - ignored_extensions = self.config.options['development'].get('ignored_extensions', '').split(',') - ignored_directories += self.config.options['development'].get('ignored_directories', '').split(',') + if 'ignored_extensions' in self.config.options['development']: + ignored_extensions = self.config.options['development'].get('ignored_extensions', '').split(',') + if 'ignored_directories' in self.config.options['development']: + ignored_directories += self.config.options['development'].get('ignored_directories', '').split(',') server = make_server(host, int(port), self) print(f'Running µWeb3 server on http://{server.server_address[0]}:{server.server_address[1]}') print(f'Root dir is: {self.executing_path}') try: if hotreload: print(f'Hot reload is enabled for changes in: {self.executing_path}') - HotReload(self.executing_path, interval=interval, dev=dev, + HotReload(self.executing_path, interval=interval, ignored_extensions=ignored_extensions, ignored_directories=ignored_directories) server.serve_forever() From f9a4852f40c1e120397363bc91515407c81c523e Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 24 May 2021 18:05:53 +0200 Subject: [PATCH 096/118] move requirements to seperate file --- requirements.txt | 3 +++ setup.py | 11 +++++------ 2 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 requirements.txt diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 00000000..a8adceee --- /dev/null +++ b/requirements.txt @@ -0,0 +1,3 @@ +PyMySQL +pytz +python-magic diff --git a/setup.py b/setup.py index d2182abb..6451b3aa 100644 --- a/setup.py +++ b/setup.py @@ -5,17 +5,16 @@ import re from setuptools import setup, find_packages -REQUIREMENTS = [ - 'PyMySQL', - 'pytz' -] +def Requirements(): + """Returns the contents of the Requirements.txt file.""" + with open(os.path.join(os.path.dirname(__file__), 'requirements.txt')) as r_file: + return r_file.read() def Description(): """Returns the contents of the README.md file as description information.""" with open(os.path.join(os.path.dirname(__file__), 'README.md')) as r_file: return r_file.read() - def Version(): """Returns the version of the library as read from the __init__.py file""" main_lib = os.path.join(os.path.dirname(__file__), 'uweb3', '__init__.py') @@ -45,5 +44,5 @@ def Version(): keywords='minimal python web framework', packages=find_packages(), include_package_data=True, - install_requires=REQUIREMENTS, + install_requires=Requirements(), python_requires='>=3.5') From f2276f5e7c5eb2e7712961de75500cf809d4fbf8 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 24 May 2021 18:07:17 +0200 Subject: [PATCH 097/118] make templateparser tests aware of new tag char ./dot, which could be useful when accesing json structures with dots in their keynames for example. Templateparser itself already handled this. --- test/test_templateparser.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/test_templateparser.py b/test/test_templateparser.py index 4f67a13b..b12903bf 100755 --- a/test/test_templateparser.py +++ b/test/test_templateparser.py @@ -217,8 +217,8 @@ def testTemplateIndexingCharacters(self): they should however not start and end with _ to avoid access to private vars. _ is allowed elsewhere in the string.""" - good_chars = "aAzZ0123-" - bad_chars = """ :~!@#$%^&*()+={}\|;':",./<>? """ + good_chars = "aAzZ0123-." + bad_chars = """ :~!@#$%^&*()+={}\|;':",/<>? """ for index in good_chars: tag = {index: 'SUCCESS'} template = '[tag:%s]' % index @@ -229,8 +229,8 @@ def testTemplateIndexingCharacters(self): self.assertEqual(self.tmpl(template).Parse(tag=tag), template) def testTemplateUnderscoreCharacters(self): - """[IndexedTag] Tags indexes may be made of word chars and dashes only, - they should however not start and end with _ to avoid access to + """[IndexedTag] Tags indexes may be made of word chars and dashes and dots + only, they should however not start and end with _ to avoid access to private vars. _ is allowed elsewhere in the string.""" # see if objects with underscores are reachable From 544de41485f112e9c8f85188606961a19792b37f Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 24 May 2021 18:09:03 +0200 Subject: [PATCH 098/118] cleanup the logging, and various other init structures to happen only when needed, only on boot, or otherwise skip them when not needed. Bump version number for new release --- uweb3/__init__.py | 148 ++++++++++++++++++++++++++++------------------ 1 file changed, 89 insertions(+), 59 deletions(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index f148e75e..26059b42 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -2,16 +2,18 @@ # -*- coding: utf-8 -*- """µWeb3 Framework""" -__version__ = '3.0.4' +__version__ = '3.0.5' # Standard modules import configparser +import datetime import logging import os import re import sys +import time +from importlib import reload from wsgiref.simple_server import make_server -import datetime # Package modules from . import pagemaker, request @@ -21,7 +23,6 @@ from .pagemaker import PageMaker, decorators, WebsocketPageMaker, DebuggingPageMaker, LoginMixin from .model import SettingsManager from .libs.safestring import HTMLsafestring, JSONsafestring, JsonEncoder, Basesafestring -from importlib import reload class Error(Exception): """Superclass used for inheritance and external exception handling.""" @@ -169,7 +170,8 @@ class uWeb: def __init__(self, page_class, routes, executing_path=None, config='config'): self.executing_path = executing_path or os.path.dirname(__file__) self.config = SettingsManager(filename=config, path=self.executing_path) - self.logger = self.setup_logger() + self._accesslogger = None + self._errorlogger = None self.inital_pagemaker = page_class self.registry = Registry() self.registry.logger = logging.getLogger('root') @@ -181,6 +183,12 @@ def __init__(self, page_class, routes, executing_path=None, config='config'): 'application/json': lambda x: JSONsafestring(x, unsafe=True), 'default': str,} + accesslogging = self.config.options.get('log', {}).get('access_logging', True) != 'False' + self._logrequest = self.logrequest if accesslogging else lambda *args: None + # log exceptions even when development is present, but error_logging was not disabled specifically + errorlogging = self.config.options.get('development', {'error_logging': 'False'}).get('error_logging', 'True') == 'True' + self._logerror = self.logerror if errorlogging else lambda *args: None + def __call__(self, env, start_response): """WSGI request handler. Accepts the WSGI `environment` dictionary and a function to start the @@ -208,9 +216,9 @@ def __call__(self, env, start_response): executing_path=self.executing_path) # specifically call _PreRequest as promised in documentation if hasattr(pagemaker_instance, '_PreRequest'): - pagemaker_instance = pagemaker_instance._PreRequest() + pagemaker_instance = pagemaker_instance._PreRequest() or pagemaker_instance - response = self.get_response(pagemaker_instance, method, args) + response = self.get_response(req, pagemaker_instance, method, args) except Exception: # something broke in our pagemaker_instance, lets fall back to the most basic pagemaker for error output if hasattr(pagemaker_instance, '_ConnectionRollback'): @@ -238,9 +246,6 @@ def __call__(self, env, start_response): encoder = self.encoders.get(response.clean_content_type(), self.encoders['default']) response.text = encoder(response.text) - if hasattr(pagemaker_instance, '_PostRequest'): - pagemaker_instance._PostRequest() - # CSP might be unneeded for some static content, # https://github.com/w3c/webappsec/issues/520 if hasattr(pagemaker_instance, '_CSPheaders'): @@ -248,44 +253,72 @@ def __call__(self, env, start_response): # provide users with a _PostRequest method to overide too if not static and hasattr(pagemaker_instance, 'PostRequest'): - response = pagemaker_instance.PostRequest(response) + response = pagemaker_instance.PostRequest(response) or response # we should at least send out something to make sure we are wsgi compliant. if not response.text: response.text = '' - self._logging(req, response) + self._logrequest(req, response) start_response(response.status, response.headerlist) try: yield response.text.encode(response.charset) except AttributeError: yield response.text - def setup_logger(self): - logger = logging.getLogger('uweb3_logger') - logger.setLevel(logging.INFO) - fh = logging.FileHandler(os.path.join(self.executing_path, 'access_logging.log')) - fh.setLevel(logging.INFO) - logger.addHandler(fh) - return logger - - def _logging(self, req, response): - """Logs incoming requests to a logfile. - This is enabled by default, even if its missing in the config file. - """ - if (self.config.options.get('development', None) and - self.config.options['development'].get('access_logging', True) == 'False'): - return - + @property + def logger(self): + if not self._accesslogger: + logger = logging.getLogger('uweb3_logger') + logger.setLevel(logging.INFO) + logpath = os.path.join(self.executing_path, self.config.options.get('log', {}).get('acces_log', 'access_log.log')) + delay = self.config.options.get('log', {}).get('acces_log_delay', False) != False + encoding = self.config.options.get('log', {}).get('acces_log_encoding', None) + fh = logging.FileHandler(logpath, encoding=encoding, delay=delay) + fh.setLevel(logging.INFO) + logger.addHandler(fh) + self._accesslogger = logger + return self._accesslogger + + @property + def errorlogger(self): + if not self._errorlogger: + logger = logging.getLogger('uweb3_exception_logger') + logger.setLevel(logging.INFO) + logpath = os.path.join(self.executing_path, self.config.options.get('log', {}).get('exception_log', 'uweb3_exceptions.log')) + delay = self.config.options.get('log', {}).get('exception_log_delay', False) != False + encoding = self.config.options.get('log', {}).get('exception_log_encoding', None) + fh = logging.FileHandler(logpath, encoding=encoding, delay=delay) + fh.setLevel(logging.INFO) + logger.addHandler(fh) + self._errorlogger = logger + return self._errorlogger + + def logrequest(self, req, response): + """Logs incoming requests to the logfile.""" host = req.env['HTTP_HOST'].split(':')[0] date = datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S') method = req.method path = req.path + get = req.vars['get'] status = response.httpcode protocol = req.env.get('SERVER_PROTOCOL') - self.logger.info(f"""{host} - - [{date}] \"{method} {path} {status} {protocol}\"""") + if not response.log: + return self.logger.info(f"""{host} - - [{date}] \"{method} {path} {get} {status} {protocol}\"""") + data = response.log + return self.logger.info(f"""{host} - - [{date}] \"{method} {path} {get} {status} {protocol} {data}\"""") - def get_response(self, page_maker, method, args): + def logerror(self, req, page_maker, pythonmethod, args): + """Logs errors and exceptions to the logfile.""" + host = req.env['HTTP_HOST'].split(':')[0] + date = datetime.datetime.now().strftime('%d/%m/%Y %H:%M:%S') + method = req.method + path = req.path + protocol = req.env.get('SERVER_PROTOCOL') + args = [str(arg) for arg in args] + return self.errorlogger.exception(f"""{host} - - [{date}] \"{method} {path} {protocol} {page_maker}.{pythonmethod}({args})\"""") + + def get_response(self, req, page_maker, method, args): try: if method != 'Static': # We're specifically calling _PostInit here as promised in documentation. @@ -303,12 +336,7 @@ def get_response(self, page_maker, method, args): except ImmediateResponse as err: return err[0] except Exception: - if (self.config.options.get('development', False) and - self.config.options['development'].get('error_logging', False) == 'True'): - logger = logging.getLogger('uweb3_exception_logger') - fh = logging.FileHandler(os.path.join(self.executing_path, 'uweb3_uncaught_exceptions.log')) - logger.addHandler(fh) - logger.exception("UNCAUGHT EXCEPTION:") + self._logerror(req, page_maker, method, args) return page_maker.InternalServerError(*sys.exc_info()) def serve(self): @@ -317,28 +345,32 @@ def serve(self): port = 8001 hotreload = False interval = None - ignored_directories = ['__pycache__', - self.inital_pagemaker.PUBLIC_DIR, - self.inital_pagemaker.TEMPLATE_DIR] - ignored_extensions = [] + if self.config.options.get('development', False): - host = self.config.options['development'].get('host', host) - port = self.config.options['development'].get('port', port) - hotreload = self.config.options['development'].get('reload', False) in ('True', 'true') - interval = int(self.config.options['development'].get('checkinterval', 0)) - if 'ignored_extensions' in self.config.options['development']: - ignored_extensions = self.config.options['development'].get('ignored_extensions', '').split(',') - if 'ignored_directories' in self.config.options['development']: - ignored_directories += self.config.options['development'].get('ignored_directories', '').split(',') + devconfig = self.config.options['development'] + host = devconfig.get('host', host) + port = devconfig.get('port', port) + hotreload = devconfig.get('reload', False) in ('True', 'true') + server = make_server(host, int(port), self) print(f'Running µWeb3 server on http://{server.server_address[0]}:{server.server_address[1]}') print(f'Root dir is: {self.executing_path}') + if hotreload: + ignored_directories = ['__pycache__', + self.inital_pagemaker.PUBLIC_DIR, + self.inital_pagemaker.TEMPLATE_DIR] + ignored_extensions = [] + interval = int(devconfig.get('checkinterval', 0)) + if 'ignored_extensions' in devconfig: + ignored_extensions = devconfig.get('ignored_extensions', '').split(',') + if 'ignored_directories' in devconfig: + ignored_directories += devconfig.get('ignored_directories', '').split(',') + + print(f'Hot reload is enabled for changes in: {self.executing_path}') + HotReload(self.executing_path, interval=interval, + ignored_extensions=ignored_extensions, + ignored_directories=ignored_directories) try: - if hotreload: - print(f'Hot reload is enabled for changes in: {self.executing_path}') - HotReload(self.executing_path, interval=interval, - ignored_extensions=ignored_extensions, - ignored_directories=ignored_directories) server.serve_forever() except Exception as error: print(error) @@ -365,18 +397,17 @@ class HotReload: execution path and restarts the server if needed""" IGNOREDEXTENSIONS = [".pyc", '.ini', '.md', '.html', '.log', '.sql'] - def __init__(self, path, interval=None, ignored_extensions=None, ignored_directories=None): + def __init__(self, path, interval=1, ignored_extensions=None, ignored_directories=None): """Takes a path, an optional interval in seconds and an optional flag signaling a development environment which will set the path for new and changed file checking on the parent folder of the serving file.""" import threading self.running = threading.Event() - self.interval = interval or 1 + self.interval = interval self.path = os.path.dirname(path) self.ignoredextensions = self.IGNOREDEXTENSIONS + (ignored_extensions or []) self.ignoreddirectories = ignored_directories - self.thread = threading.Thread(target=self.Run) - self.thread.daemon = True + self.thread = threading.Thread(target=self.Run, daemon=True) self.thread.start() def Run(self): @@ -384,7 +415,6 @@ def Run(self): self.watched_files = self.Files() self.mtimes = [(f, os.path.getmtime(f)) for f in self.watched_files] - import time while True: time.sleep(self.interval) new = self.Files(self.watched_files) @@ -397,7 +427,7 @@ def Run(self): self.Restart() def Files(self, current=None): - """Returns all files inside the working directory of uweb3.""" + """Returns all files inside the working directory of µWeb3.""" if not current: current = set() new = set() @@ -414,6 +444,6 @@ def Files(self, current=None): return new def Restart(self): - """Restart uweb3 with all provided system arguments.""" + """Restart µWeb3 with all provided system arguments.""" self.running.clear() os.execl(sys.executable, sys.executable, * sys.argv) From 97b1441101e8abecd9d9c58803fb1afccd108aea Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 24 May 2021 18:09:45 +0200 Subject: [PATCH 099/118] remove unneeded utils file --- uweb3/libs/utils.py | 775 -------------------------------------------- 1 file changed, 775 deletions(-) delete mode 100644 uweb3/libs/utils.py diff --git a/uweb3/libs/utils.py b/uweb3/libs/utils.py deleted file mode 100644 index b4aaf570..00000000 --- a/uweb3/libs/utils.py +++ /dev/null @@ -1,775 +0,0 @@ -# -*- coding: utf-8 -*- -""" - werkzeug.utils - ~~~~~~~~~~~~~~ - - This module implements various utilities for WSGI applications. Most of - them are used by the request and response wrappers but especially for - middleware development it makes sense to use them without the wrappers. - - :copyright: 2007 Pallets - :license: BSD-3-Clause -""" -import codecs -import os -import pkgutil -import re -import sys - -from ._compat import iteritems -from ._compat import PY2 -from ._compat import reraise -from ._compat import string_types -from ._compat import text_type -from ._compat import unichr -from ._internal import _DictAccessorProperty -from ._internal import _missing -from ._internal import _parse_signature - -try: - from html.entities import name2codepoint -except ImportError: - from htmlentitydefs import name2codepoint - - -_format_re = re.compile(r"\$(?:(%s)|\{(%s)\})" % (("[a-zA-Z_][a-zA-Z0-9_]*",) * 2)) -_entity_re = re.compile(r"&([^;]+);") -_filename_ascii_strip_re = re.compile(r"[^A-Za-z0-9_.-]") -_windows_device_files = ( - "CON", - "AUX", - "COM1", - "COM2", - "COM3", - "COM4", - "LPT1", - "LPT2", - "LPT3", - "PRN", - "NUL", -) - - -class cached_property(property): - """A decorator that converts a function into a lazy property. The - function wrapped is called the first time to retrieve the result - and then that calculated result is used the next time you access - the value:: - - class Foo: - - @cached_property - def foo(self): - # calculate something important here - return 42 - - The class has to have a `__dict__` in order for this property to - work. - """ - - # implementation detail: A subclass of python's builtin property - # decorator, we override __get__ to check for a cached value. If one - # chooses to invoke __get__ by hand the property will still work as - # expected because the lookup logic is replicated in __get__ for - # manual invocation. - - def __init__(self, func, name=None, doc=None): - self.__name__ = name or func.__name__ - self.__module__ = func.__module__ - self.__doc__ = doc or func.__doc__ - self.func = func - - def __set__(self, obj, value): - obj.__dict__[self.__name__] = value - - def __get__(self, obj, type=None): - if obj is None: - return self - value = obj.__dict__.get(self.__name__, _missing) - if value is _missing: - value = self.func(obj) - obj.__dict__[self.__name__] = value - return value - - -class environ_property(_DictAccessorProperty): - """Maps request attributes to environment variables. This works not only - for the Werzeug request object, but also any other class with an - environ attribute: - - >>> class Test: - ... environ = {'key': 'value'} - ... test = environ_property('key') - >>> var = Test() - >>> var.test - 'value' - - If you pass it a second value it's used as default if the key does not - exist, the third one can be a converter that takes a value and converts - it. If it raises :exc:`ValueError` or :exc:`TypeError` the default value - is used. If no default value is provided `None` is used. - - Per default the property is read only. You have to explicitly enable it - by passing ``read_only=False`` to the constructor. - """ - - read_only = True - - def lookup(self, obj): - return obj.environ - - -class header_property(_DictAccessorProperty): - """Like `environ_property` but for headers.""" - - def lookup(self, obj): - return obj.headers - - -class HTMLBuilder: - """Helper object for HTML generation. - - Per default there are two instances of that class. The `html` one, and - the `xhtml` one for those two dialects. The class uses keyword parameters - and positional parameters to generate small snippets of HTML. - - Keyword parameters are converted to XML/SGML attributes, positional - arguments are used as children. Because Python accepts positional - arguments before keyword arguments it's a good idea to use a list with the - star-syntax for some children: - - >>> html.p(class_='foo', *[html.a('foo', href='foo.html'), ' ', - ... html.a('bar', href='bar.html')]) - u'

foo bar

' - - This class works around some browser limitations and can not be used for - arbitrary SGML/XML generation. For that purpose lxml and similar - libraries exist. - - Calling the builder escapes the string passed: - - >>> html.p(html("")) - u'

<foo>

' - """ - - _entity_re = re.compile(r"&([^;]+);") - _entities = name2codepoint.copy() - _entities["apos"] = 39 - _empty_elements = { - "area", - "base", - "basefont", - "br", - "col", - "command", - "embed", - "frame", - "hr", - "img", - "input", - "keygen", - "isindex", - "link", - "meta", - "param", - "source", - "wbr", - } - _boolean_attributes = { - "selected", - "checked", - "compact", - "declare", - "defer", - "disabled", - "ismap", - "multiple", - "nohref", - "noresize", - "noshade", - "nowrap", - } - _plaintext_elements = {"textarea"} - _c_like_cdata = {"script", "style"} - - def __init__(self, dialect): - self._dialect = dialect - - def __call__(self, s): - return escape(s) - - def __getattr__(self, tag): - if tag[:2] == "__": - raise AttributeError(tag) - - def proxy(*children, **arguments): - buffer = "<" + tag - for key, value in iteritems(arguments): - if value is None: - continue - if key[-1] == "_": - key = key[:-1] - if key in self._boolean_attributes: - if not value: - continue - if self._dialect == "xhtml": - value = '="' + key + '"' - else: - value = "" - else: - value = '="' + escape(value) + '"' - buffer += " " + key + value - if not children and tag in self._empty_elements: - if self._dialect == "xhtml": - buffer += " />" - else: - buffer += ">" - return buffer - buffer += ">" - - children_as_string = "".join( - text_type(x) for x in children if x is not None - ) - - if children_as_string: - if tag in self._plaintext_elements: - children_as_string = escape(children_as_string) - elif tag in self._c_like_cdata and self._dialect == "xhtml": - children_as_string = ( - "/**/" - ) - buffer += children_as_string + "" - return buffer - - return proxy - - def __repr__(self): - return "<%s for %r>" % (self.__class__.__name__, self._dialect) - - -html = HTMLBuilder("html") -xhtml = HTMLBuilder("xhtml") - -# https://cgit.freedesktop.org/xdg/shared-mime-info/tree/freedesktop.org.xml.in -# https://www.iana.org/assignments/media-types/media-types.xhtml -# Types listed in the XDG mime info that have a charset in the IANA registration. -_charset_mimetypes = { - "application/ecmascript", - "application/javascript", - "application/sql", - "application/xml", - "application/xml-dtd", - "application/xml-external-parsed-entity", -} - - -def get_content_type(mimetype, charset): - """Returns the full content type string with charset for a mimetype. - - If the mimetype represents text, the charset parameter will be - appended, otherwise the mimetype is returned unchanged. - - :param mimetype: The mimetype to be used as content type. - :param charset: The charset to be appended for text mimetypes. - :return: The content type. - - .. verionchanged:: 0.15 - Any type that ends with ``+xml`` gets a charset, not just those - that start with ``application/``. Known text types such as - ``application/javascript`` are also given charsets. - """ - if ( - mimetype.startswith("text/") - or mimetype in _charset_mimetypes - or mimetype.endswith("+xml") - ): - mimetype += "; charset=" + charset - - return mimetype - - -def detect_utf_encoding(data): - """Detect which UTF encoding was used to encode the given bytes. - - The latest JSON standard (:rfc:`8259`) suggests that only UTF-8 is - accepted. Older documents allowed 8, 16, or 32. 16 and 32 can be big - or little endian. Some editors or libraries may prepend a BOM. - - :internal: - - :param data: Bytes in unknown UTF encoding. - :return: UTF encoding name - - .. versionadded:: 0.15 - """ - head = data[:4] - - if head[:3] == codecs.BOM_UTF8: - return "utf-8-sig" - - if b"\x00" not in head: - return "utf-8" - - if head in (codecs.BOM_UTF32_BE, codecs.BOM_UTF32_LE): - return "utf-32" - - if head[:2] in (codecs.BOM_UTF16_BE, codecs.BOM_UTF16_LE): - return "utf-16" - - if len(head) == 4: - if head[:3] == b"\x00\x00\x00": - return "utf-32-be" - - if head[::2] == b"\x00\x00": - return "utf-16-be" - - if head[1:] == b"\x00\x00\x00": - return "utf-32-le" - - if head[1::2] == b"\x00\x00": - return "utf-16-le" - - if len(head) == 2: - return "utf-16-be" if head.startswith(b"\x00") else "utf-16-le" - - return "utf-8" - - -def format_string(string, context): - """String-template format a string: - - >>> format_string('$foo and ${foo}s', dict(foo=42)) - '42 and 42s' - - This does not do any attribute lookup etc. For more advanced string - formattings have a look at the `werkzeug.template` module. - - :param string: the format string. - :param context: a dict with the variables to insert. - """ - - def lookup_arg(match): - x = context[match.group(1) or match.group(2)] - if not isinstance(x, string_types): - x = type(string)(x) - return x - - return _format_re.sub(lookup_arg, string) - - -def secure_filename(filename): - r"""Pass it a filename and it will return a secure version of it. This - filename can then safely be stored on a regular file system and passed - to :func:`os.path.join`. The filename returned is an ASCII only string - for maximum portability. - - On windows systems the function also makes sure that the file is not - named after one of the special device files. - - >>> secure_filename("My cool movie.mov") - 'My_cool_movie.mov' - >>> secure_filename("../../../etc/passwd") - 'etc_passwd' - >>> secure_filename(u'i contain cool \xfcml\xe4uts.txt') - 'i_contain_cool_umlauts.txt' - - The function might return an empty filename. It's your responsibility - to ensure that the filename is unique and that you abort or - generate a random filename if the function returned an empty one. - - .. versionadded:: 0.5 - - :param filename: the filename to secure - """ - if isinstance(filename, text_type): - from unicodedata import normalize - - filename = normalize("NFKD", filename).encode("ascii", "ignore") - if not PY2: - filename = filename.decode("ascii") - for sep in os.path.sep, os.path.altsep: - if sep: - filename = filename.replace(sep, " ") - filename = str(_filename_ascii_strip_re.sub("", "_".join(filename.split()))).strip( - "._" - ) - - # on nt a couple of special files are present in each folder. We - # have to ensure that the target file is not such a filename. In - # this case we prepend an underline - if ( - os.name == "nt" - and filename - and filename.split(".")[0].upper() in _windows_device_files - ): - filename = "_" + filename - - return filename - - -def escape(s, quote=None): - """Replace special characters "&", "<", ">" and (") to HTML-safe sequences. - - There is a special handling for `None` which escapes to an empty string. - - .. versionchanged:: 0.9 - `quote` is now implicitly on. - - :param s: the string to escape. - :param quote: ignored. - """ - if s is None: - return "" - elif hasattr(s, "__html__"): - return text_type(s.__html__()) - elif not isinstance(s, string_types): - s = text_type(s) - if quote is not None: - from warnings import warn - - warn( - "The 'quote' parameter is no longer used as of version 0.9" - " and will be removed in version 1.0.", - DeprecationWarning, - stacklevel=2, - ) - s = ( - s.replace("&", "&") - .replace("<", "<") - .replace(">", ">") - .replace('"', """) - ) - return s - - -def unescape(s): - """The reverse function of `escape`. This unescapes all the HTML - entities, not only the XML entities inserted by `escape`. - - :param s: the string to unescape. - """ - - def handle_match(m): - name = m.group(1) - if name in HTMLBuilder._entities: - return unichr(HTMLBuilder._entities[name]) - try: - if name[:2] in ("#x", "#X"): - return unichr(int(name[2:], 16)) - elif name.startswith("#"): - return unichr(int(name[1:])) - except ValueError: - pass - return u"" - - return _entity_re.sub(handle_match, s) - - -def redirect(location, code=302, Response=None): - """Returns a response object (a WSGI application) that, if called, - redirects the client to the target location. Supported codes are - 301, 302, 303, 305, 307, and 308. 300 is not supported because - it's not a real redirect and 304 because it's the answer for a - request with a request with defined If-Modified-Since headers. - - .. versionadded:: 0.6 - The location can now be a unicode string that is encoded using - the :func:`iri_to_uri` function. - - .. versionadded:: 0.10 - The class used for the Response object can now be passed in. - - :param location: the location the response should redirect to. - :param code: the redirect status code. defaults to 302. - :param class Response: a Response class to use when instantiating a - response. The default is :class:`werkzeug.wrappers.Response` if - unspecified. - """ - if Response is None: - from .wrappers import Response - - display_location = escape(location) - if isinstance(location, text_type): - # Safe conversion is necessary here as we might redirect - # to a broken URI scheme (for instance itms-services). - from .urls import iri_to_uri - - location = iri_to_uri(location, safe_conversion=True) - response = Response( - '\n' - "Redirecting...\n" - "

Redirecting...

\n" - "

You should be redirected automatically to target URL: " - '%s. If not click the link.' - % (escape(location), display_location), - code, - mimetype="text/html", - ) - response.headers["Location"] = location - return response - - -def append_slash_redirect(environ, code=301): - """Redirects to the same URL but with a slash appended. The behavior - of this function is undefined if the path ends with a slash already. - - :param environ: the WSGI environment for the request that triggers - the redirect. - :param code: the status code for the redirect. - """ - new_path = environ["PATH_INFO"].strip("/") + "/" - query_string = environ.get("QUERY_STRING") - if query_string: - new_path += "?" + query_string - return redirect(new_path, code) - - -def import_string(import_name, silent=False): - """Imports an object based on a string. This is useful if you want to - use import paths as endpoints or something similar. An import path can - be specified either in dotted notation (``xml.sax.saxutils.escape``) - or with a colon as object delimiter (``xml.sax.saxutils:escape``). - - If `silent` is True the return value will be `None` if the import fails. - - :param import_name: the dotted name for the object to import. - :param silent: if set to `True` import errors are ignored and - `None` is returned instead. - :return: imported object - """ - # force the import name to automatically convert to strings - # __import__ is not able to handle unicode strings in the fromlist - # if the module is a package - import_name = str(import_name).replace(":", ".") - try: - try: - __import__(import_name) - except ImportError: - if "." not in import_name: - raise - else: - return sys.modules[import_name] - - module_name, obj_name = import_name.rsplit(".", 1) - module = __import__(module_name, globals(), locals(), [obj_name]) - try: - return getattr(module, obj_name) - except AttributeError as e: - raise ImportError(e) - - except ImportError as e: - if not silent: - reraise( - ImportStringError, ImportStringError(import_name, e), sys.exc_info()[2] - ) - - -def find_modules(import_path, include_packages=False, recursive=False): - """Finds all the modules below a package. This can be useful to - automatically import all views / controllers so that their metaclasses / - function decorators have a chance to register themselves on the - application. - - Packages are not returned unless `include_packages` is `True`. This can - also recursively list modules but in that case it will import all the - packages to get the correct load path of that module. - - :param import_path: the dotted name for the package to find child modules. - :param include_packages: set to `True` if packages should be returned, too. - :param recursive: set to `True` if recursion should happen. - :return: generator - """ - module = import_string(import_path) - path = getattr(module, "__path__", None) - if path is None: - raise ValueError("%r is not a package" % import_path) - basename = module.__name__ + "." - for _importer, modname, ispkg in pkgutil.iter_modules(path): - modname = basename + modname - if ispkg: - if include_packages: - yield modname - if recursive: - yield from find_modules(modname, include_packages, True) - else: - yield modname - - -def validate_arguments(func, args, kwargs, drop_extra=True): - """Checks if the function accepts the arguments and keyword arguments. - Returns a new ``(args, kwargs)`` tuple that can safely be passed to - the function without causing a `TypeError` because the function signature - is incompatible. If `drop_extra` is set to `True` (which is the default) - any extra positional or keyword arguments are dropped automatically. - - The exception raised provides three attributes: - - `missing` - A set of argument names that the function expected but where - missing. - - `extra` - A dict of keyword arguments that the function can not handle but - where provided. - - `extra_positional` - A list of values that where given by positional argument but the - function cannot accept. - - This can be useful for decorators that forward user submitted data to - a view function:: - - from werkzeug.utils import ArgumentValidationError, validate_arguments - - def sanitize(f): - def proxy(request): - data = request.values.to_dict() - try: - args, kwargs = validate_arguments(f, (request,), data) - except ArgumentValidationError: - raise BadRequest('The browser failed to transmit all ' - 'the data expected.') - return f(*args, **kwargs) - return proxy - - :param func: the function the validation is performed against. - :param args: a tuple of positional arguments. - :param kwargs: a dict of keyword arguments. - :param drop_extra: set to `False` if you don't want extra arguments - to be silently dropped. - :return: tuple in the form ``(args, kwargs)``. - """ - parser = _parse_signature(func) - args, kwargs, missing, extra, extra_positional = parser(args, kwargs)[:5] - if missing: - raise ArgumentValidationError(tuple(missing)) - elif (extra or extra_positional) and not drop_extra: - raise ArgumentValidationError(None, extra, extra_positional) - return tuple(args), kwargs - - -def bind_arguments(func, args, kwargs): - """Bind the arguments provided into a dict. When passed a function, - a tuple of arguments and a dict of keyword arguments `bind_arguments` - returns a dict of names as the function would see it. This can be useful - to implement a cache decorator that uses the function arguments to build - the cache key based on the values of the arguments. - - :param func: the function the arguments should be bound for. - :param args: tuple of positional arguments. - :param kwargs: a dict of keyword arguments. - :return: a :class:`dict` of bound keyword arguments. - """ - ( - args, - kwargs, - missing, - extra, - extra_positional, - arg_spec, - vararg_var, - kwarg_var, - ) = _parse_signature(func)(args, kwargs) - values = { - name: value - for (name, _has_default, _default), value in zip(arg_spec, args) - } - - if vararg_var is not None: - values[vararg_var] = tuple(extra_positional) - elif extra_positional: - raise TypeError("too many positional arguments") - if kwarg_var is not None: - multikw = set(extra) & {x[0] for x in arg_spec} - if multikw: - raise TypeError( - "got multiple values for keyword argument " + repr(next(iter(multikw))) - ) - values[kwarg_var] = extra - elif extra: - raise TypeError("got unexpected keyword argument " + repr(next(iter(extra)))) - return values - - -class ArgumentValidationError(ValueError): - - """Raised if :func:`validate_arguments` fails to validate""" - - def __init__(self, missing=None, extra=None, extra_positional=None): - self.missing = set(missing or ()) - self.extra = extra or {} - self.extra_positional = extra_positional or [] - ValueError.__init__( - self, - "function arguments invalid. (%d missing, %d additional)" - % (len(self.missing), len(self.extra) + len(self.extra_positional)), - ) - - -class ImportStringError(ImportError): - """Provides information about a failed :func:`import_string` attempt.""" - - #: String in dotted notation that failed to be imported. - import_name = None - #: Wrapped exception. - exception = None - - def __init__(self, import_name, exception): - self.import_name = import_name - self.exception = exception - - msg = ( - "import_string() failed for %r. Possible reasons are:\n\n" - "- missing __init__.py in a package;\n" - "- package or module path not included in sys.path;\n" - "- duplicated package or module name taking precedence in " - "sys.path;\n" - "- missing module, class, function or variable;\n\n" - "Debugged import:\n\n%s\n\n" - "Original exception:\n\n%s: %s" - ) - - name = "" - tracked = [] - for part in import_name.replace(":", ".").split("."): - name += (name and ".") + part - imported = import_string(name, silent=True) - if imported: - tracked.append((name, getattr(imported, "__file__", None))) - else: - track = ["- %r found in %r." % (n, i) for n, i in tracked] - track.append("- %r not found." % name) - msg %= ( - import_name, - "\n".join(track), - exception.__class__.__name__, - str(exception), - ) - break - - ImportError.__init__(self, msg) - - def __repr__(self): - return "<%s(%r, %r)>" % ( - self.__class__.__name__, - self.import_name, - self.exception, - ) - - -from werkzeug import _DeprecatedImportModule - -_DeprecatedImportModule( - __name__, - { - ".datastructures": [ - "CombinedMultiDict", - "EnvironHeaders", - "Headers", - "MultiDict", - ], - ".http": ["dump_cookie", "parse_cookie"], - }, - "Werkzeug 1.0", -) -del _DeprecatedImportModule From 0cc95b5fe8258dcf750d79fc40a560bfa2133e78 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 24 May 2021 18:10:43 +0200 Subject: [PATCH 100/118] use json instead of pickle on SignedCookies, remove some unneeded debugging output in scope finder --- uweb3/connections.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/uweb3/connections.py b/uweb3/connections.py index 2f4d54d2..5e4a6851 100644 --- a/uweb3/connections.py +++ b/uweb3/connections.py @@ -130,10 +130,7 @@ def request(self): try: parent = sys._getframe(requestdepth).f_locals['self'] if isinstance(parent, uweb3.PageMaker) and hasattr(parent, 'req'): - request = parent.req - if self.debug: - print('request object found at stack level %d' % requestdepth) - return request + return parent.req except (KeyError, AttributeError, ValueError): pass requestdepth = requestdepth + 1 From 1431faacd3f781f20d58ac52a3fcc0bbd8da1b71 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 24 May 2021 18:11:38 +0200 Subject: [PATCH 101/118] use json instead of pickle on SignedCookies --- uweb3/model.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/uweb3/model.py b/uweb3/model.py index 433ab814..69c5d921 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -6,12 +6,10 @@ import datetime import sys import hashlib -import pickle +import json import secrets import configparser -from contextlib import contextmanager - class Error(Exception): """Superclass used for inheritance and external exception handling.""" @@ -114,7 +112,9 @@ def Create(self, section, key, value): def Read(self): """Reads the config file and populates the options member - It uses the mtime to see if any re-reading is required""" + It uses the mtime to see if any re-reading is required + + Returns True if changes where detected, False if no re-read was needed.""" if not self.mtime: curtime = os.path.getmtime(self.file_location) if self.mtime and self.mtime == curtime: @@ -335,12 +335,10 @@ def Delete(self): self._rawcookie = None def __CreateCookieHash(self, data): - hex_string = pickle.dumps(data).hex() - - hashed = (hex_string + self.cookie_salt).encode('utf-8') + datastring = json.dumps(data) h = hashlib.new(self.HASHTYPE) - h.update(hashed) - return '{}+{}'.format(h.hexdigest(), hex_string) + h.update((datastring + self.cookie_salt).encode('utf-8')) + return '{}+{}'.format(h.hexdigest(), datastring) def __ValidateCookieHash(self, cookie): """Takes a cookie and validates it @@ -351,8 +349,7 @@ def __ValidateCookieHash(self, cookie): if not cookie: return None try: - data = cookie.rsplit('+', 1)[1] - data = pickle.loads(bytes.fromhex(data)) + data = json.loads(cookie.rsplit('+', 1)[1]) except Exception: return (False, None) From 911abf06040ae06a871107ebacc1fc33ceeaf0b6 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 24 May 2021 18:12:29 +0200 Subject: [PATCH 102/118] use Magic module to find content-type for static files, and serve correct mime-type based headers --- uweb3/pagemaker/__init__.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index ec962928..3f6759a7 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -3,6 +3,7 @@ import datetime import logging +import magic import mimetypes import os import pyclbr @@ -314,6 +315,7 @@ def __init__(self, self.post = req.vars['post'] if 'post' in req.vars else {} self.put = req.vars['put'] if 'put' in req.vars else {} self.delete = req.vars['delete'] if 'delete' in req.vars else {} + self.files = req.vars['files'] if 'files' in req.vars else {} self.config = config or None self.options = config.options if config else {} self.debug = DebuggerMixin in self.__class__.__mro__ @@ -323,6 +325,9 @@ def __init__(self, self.persistent.Set('connection', ConnectionManager(self.config, self.options, self.debug)) self.connection = self.persistent.Get('connection') + def __str__(self): + return str(type(self)) + @classmethod def LoadModules(cls, routes='routes/*.py'): """Loops over all .py files apart from some exceptions in target directory @@ -383,14 +388,17 @@ def Static(self, rel_path): """ abs_path = os.path.realpath(os.path.join(self.PUBLIC_DIR, rel_path.lstrip('/'))) - print(abs_path, self.PUBLIC_DIR) + if self.debug: + print('Serving static file:', abs_path) if os.path.commonprefix((abs_path, self.PUBLIC_DIR)) != self.PUBLIC_DIR: return self._StaticNotFound(rel_path) try: content_type, _encoding = mimetypes.guess_type(abs_path) if not content_type: - content_type = 'text/plain' + content_type = magic.from_file(abs_path, mime=True) + if not content_type: + content_type = 'text/plain' mtime = os.path.getmtime(abs_path) length = os.path.getsize(abs_path) with open(abs_path, 'rb') as staticfile: From e0b347b5e9ee3fc4e04c1143437657f5c4ffde26 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 25 May 2021 10:30:15 +0200 Subject: [PATCH 103/118] add encoding / decoding to secureCookie as not all json chars are valid in cookies --- uweb3/model.py | 39 ++++++++++++++++++++++++++++++++------- 1 file changed, 32 insertions(+), 7 deletions(-) diff --git a/uweb3/model.py b/uweb3/model.py index 69c5d921..e00a528c 100644 --- a/uweb3/model.py +++ b/uweb3/model.py @@ -2,13 +2,15 @@ """uWeb3 model base classes.""" # Standard modules -import os +import base64 +import configparser import datetime +import os import sys import hashlib import json import secrets -import configparser + class Error(Exception): @@ -324,7 +326,9 @@ def Update(self, data, **attrs): if not self.rawcookie: raise ValueError("No valid cookie with name `{}` found".format(name)) self._rawcookie = data - self.request.AddCookie(name, self.__CreateCookieHash(data), **attrs) + hashed = self.__CreateCookieHash(data) + self.cookies[name] = hashed + self.request.AddCookie(name, hashed, **attrs) def Delete(self): """Deletes cookie based on name @@ -335,10 +339,10 @@ def Delete(self): self._rawcookie = None def __CreateCookieHash(self, data): - datastring = json.dumps(data) + data = str(json.dumps(data)) h = hashlib.new(self.HASHTYPE) - h.update((datastring + self.cookie_salt).encode('utf-8')) - return '{}+{}'.format(h.hexdigest(), datastring) + h.update((data + self.cookie_salt).encode('utf-8')) + return '{}+{}'.format(h.hexdigest(), self._encode(data)) def __ValidateCookieHash(self, cookie): """Takes a cookie and validates it @@ -349,14 +353,35 @@ def __ValidateCookieHash(self, cookie): if not cookie: return None try: - data = json.loads(cookie.rsplit('+', 1)[1]) + data = json.loads(self._decode(cookie.rsplit('+', 1)[1])) except Exception: + print('Cookie contents could not be loaded as Json') return (False, None) if cookie == self.__CreateCookieHash(data): return (True, data) + print('Cookie contents could not be verified as hash is different') return (False, None) + @staticmethod + def _encode(data): + """Encode cookie values per RFC 6265 + http://www.ietf.org/rfc/rfc6265.txt + + We elect to only encode the control chars for the cookie spec, and not the + whole cookie content. + """ + return data.replace('%', "%25").replace('"', "%22").replace(",", "%27").replace('{', "%7B").replace('}', "%7D").replace('=', "%3D") + + @staticmethod + def _decode(data): + """decode cookie values per RFC 6265 + http://www.ietf.org/rfc/rfc6265.txt + + We elect to only decode the control chars for the cookie spec, and not the + whole cookie content. + """ + return data.replace("%22", '"').replace("%27", ",").replace("%7B", '{').replace("%7D", '}').replace("%3D", '=').replace("%25", '%') # Record classes have many methods, this is not an actual problem. # pylint: disable=R0904 From f013161e7c9254df03b914b121c3d7ccdfb7082c Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 25 May 2021 10:31:31 +0200 Subject: [PATCH 104/118] add log variable to response which allows the user to set custom log lines on the response --- uweb3/response.py | 1 + 1 file changed, 1 insertion(+) diff --git a/uweb3/response.py b/uweb3/response.py index 9c4f52c4..568e99ff 100644 --- a/uweb3/response.py +++ b/uweb3/response.py @@ -34,6 +34,7 @@ def __init__(self, content='', content_type=CONTENT_TYPE, self.charset = kwds.get('charset', 'utf-8') self.content = content self.httpcode = httpcode + self.log = None self.headers = headers or {} if (';' not in content_type and (content_type.startswith('text/') or From a0faabe14c51a2abdfe37421dc49d80d7d41241b Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 25 May 2021 10:34:34 +0200 Subject: [PATCH 105/118] add default encoder for xml (svg/xml) outputs which handles mime-types ending in /xml or +xml. Rename the PostRequest handler on the connection manager to CloseRequestConnections and use that after each request to reset the cookie/request connector. add extra argument to static handler to overwrite guessed mime-types, defaults to none, and thus guessing --- uweb3/__init__.py | 6 ++++-- uweb3/pagemaker/__init__.py | 26 +++++++++++++++++--------- 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 26059b42..0f9fa766 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -180,8 +180,9 @@ def __init__(self, page_class, routes, executing_path=None, config='config'): self.encoders = { 'text/html': lambda x: HTMLsafestring(x, unsafe=True), 'text/plain': str, + 'text/csv': str, 'application/json': lambda x: JSONsafestring(x, unsafe=True), - 'default': str,} + 'default': lambda x: HTMLsafestring(x, unsafe=True) if str(x).endswith('xml') else str(x)} accesslogging = self.config.options.get('log', {}).get('access_logging', True) != 'False' self._logrequest = self.logrequest if accesslogging else lambda *args: None @@ -251,9 +252,10 @@ def __call__(self, env, start_response): if hasattr(pagemaker_instance, '_CSPheaders'): pagemaker_instance._CSPheaders() - # provide users with a _PostRequest method to overide too + # provide users with a PostRequest method to overide too if not static and hasattr(pagemaker_instance, 'PostRequest'): response = pagemaker_instance.PostRequest(response) or response + pagemaker_instance.CloseRequestConnections() # we should at least send out something to make sure we are wsgi compliant. if not response.text: diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 3f6759a7..d13520ea 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -368,19 +368,23 @@ def __SetupPaths(cls, executing_path): cls.PUBLIC_DIR = os.path.realpath(os.path.join(cls_dir, cls.PUBLIC_DIR)) cls.TEMPLATE_DIR = os.path.realpath(os.path.join(cls_dir, cls.TEMPLATE_DIR)) - def Static(self, rel_path): + def Static(self, rel_path, content_type=None): """Provides a handler for static content. The requested `path` is truncated against a root (removing any uplevels), and then added to the working dir + PUBLIC_DIR. If the request file exists, - then the requested file is retrieved, its mimetype guessed, and returned - to the client performing the request. + then the requested file is retrieved, if needed mimetype guessed, and + returned to the client performing the request. Should the requested file not exist, a 404 page is returned instead. Arguments: @ rel_path: str The filename relative to the working directory of the webserver. + @ content_type: str + The content_type we will send to the client, If None it will be guessed + based on the extention or by the contents of the file (in that order). + If no guess can be made, text/plain will be used. Returns: Page: contains the content and mimetype of the requested file, or a 404 @@ -394,14 +398,17 @@ def Static(self, rel_path): if os.path.commonprefix((abs_path, self.PUBLIC_DIR)) != self.PUBLIC_DIR: return self._StaticNotFound(rel_path) try: - content_type, _encoding = mimetypes.guess_type(abs_path) + if not content_type: + content_type, _encoding = mimetypes.guess_type(abs_path) if not content_type: content_type = magic.from_file(abs_path, mime=True) - if not content_type: - content_type = 'text/plain' + if not content_type: + content_type = 'text/plain' mtime = os.path.getmtime(abs_path) length = os.path.getsize(abs_path) - with open(abs_path, 'rb') as staticfile: + readtype = 'r' if content_type.startswith('text/') else 'rb' + + with open(abs_path, readtype) as staticfile: cache_days = self.CACHE_DURATION.get(content_type, 0) expires = datetime.datetime.utcnow() + datetime.timedelta(cache_days) return response.Response(content=staticfile.read(), @@ -438,8 +445,9 @@ def Reload(): """Raises `ReloadModules`, telling the Handler() to reload its pageclass.""" raise ReloadModules('Reloading ... ') - def _PostRequest(self): - """Method that gets called after each request""" + def CloseRequestConnections(self): + """Method that gets called after each request to close 'request' based + connections like signedcookieStores""" self.connection.PostRequest() From b8484c440f278a51873280889961090b94012fa5 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 25 May 2021 10:36:33 +0200 Subject: [PATCH 106/118] small cleanups to templateparser, better restrict the templateparser's template inclusion to the templatedir, add some allowed functions to the eval list --- uweb3/templateparser.py | 30 ++++++++++++++++-------------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index 8fb753b5..be73fe8b 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -11,7 +11,7 @@ """ __author__ = ('Elmer de Looff ', 'Jan Klopper ') -__version__ = '1.6' +__version__ = '1.7' # Standard modules import os @@ -111,7 +111,7 @@ def values(self): EVALWHITELIST = { 'functions': {"abs": abs, "complex": complex, "min": min, "max": max, "pow": pow, "round": round, "len": len, "type": type, - "isinstance": isinstance, + "isinstance": isinstance, "list": list, **{key: value for (key,value) in vars(math).items() if not key.startswith('__')}}, 'operators': (ast.Module, ast.Expr, ast.Load, ast.Expression, ast.Add, ast.And, ast.Sub, ast.UnaryOp, ast.Num, ast.BinOp, ast.Mult, ast.Gt, ast.GtE, @@ -142,7 +142,7 @@ class Parser(dict): providing the `RegisterFunction` method to add or replace functions in this module constant. """ - def __init__(self, path='.', templates=(), noparse=False, templateEncoding='utf-8'): + def __init__(self, path=None, templates=(), noparse=False, templateEncoding='utf-8'): """Initializes a Parser instance. This sets up the template directory and preloads any templates given. @@ -205,9 +205,12 @@ def AddTemplate(self, location, name=None): Raises: TemplateReadError: When the template file cannot be read """ - template_path = os.path.realpath(os.path.join(self.template_dir, location)) - if os.path.commonprefix((template_path, self.template_dir)) != self.template_dir: - raise TemplateReadError('Could not load template %r' % template_path) + if self.template_dir: + template_path = os.path.realpath(os.path.join(self.template_dir, location)) + if os.path.commonprefix((template_path, self.template_dir)) != self.template_dir: + raise TemplateReadError('Could not load template %r, not in %r' % (template_path, self.template_dir)) + else: + template_path = location try: self[name or location] = FileTemplate(template_path, parser=self, encoding=None) except IOError: @@ -571,8 +574,8 @@ def __init__(self, template_path, parser=None, encoding='utf-8'): with open(self._file_name, encoding=self.templateEncoding) as templatefile: raw_template = templatefile.read() super().__init__(raw_template, parser=parser) - except (IOError, OSError): - raise TemplateReadError('Cannot open: %r' % template_path) + except (IOError, OSError) as error: + raise TemplateReadError('Cannot open: %r %r' % (template_path, error)) def Parse(self, **kwds): """Returns the parsed template as SafeString. @@ -766,15 +769,14 @@ def __init__(self, tag, aliases): @ aliases: *str The alias(es) under which the loop variable should be made available. """ - try: - tag = TemplateTag.FromString(tag) - except TemplateSyntaxError: - raise TemplateSyntaxError('Tag %r in {{ for }} loop is not valid' % tag) - super().__init__() self.aliases = ''.join(aliases).split(',') self.aliascount = len(self.aliases) - self.tag = tag + try: + self.tag = TemplateTag.FromString(tag) + except TemplateSyntaxError: + self.tag = tag + raise TemplateSyntaxError('Tag %r in {{ for }} loop is not valid' % tag) def __repr__(self): return '%s(%s)' % (type(self).__name__, list(self)) From b858034c9cceaf9e9d91c75cb5ff337faba20651 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 25 May 2021 10:38:21 +0200 Subject: [PATCH 107/118] add upload limiter lib, and use it to parse multipart forms, which allows for file uploads. unset the files member on the pagemaker for incorrect xsrf token requests in its checking decorator. --- uweb3/pagemaker/decorators.py | 4 +- uweb3/request.py | 79 +++++++++++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 9 deletions(-) diff --git a/uweb3/pagemaker/decorators.py b/uweb3/pagemaker/decorators.py index 07137a03..fd432b22 100644 --- a/uweb3/pagemaker/decorators.py +++ b/uweb3/pagemaker/decorators.py @@ -23,7 +23,7 @@ def wrapper(*args, **kwargs): def checkxsrf(f): """Decorator that checks the user's XSRF. The function will compare the XSRF in the user's cookie and in the - (post) request. Make sure to have xsrf_enabled = True in the config.ini + (post) request. """ def _clear_form_data(pagemaker): method = pagemaker.req.method.lower() @@ -33,6 +33,8 @@ def _clear_form_data(pagemaker): setattr(pagemaker, method, IndexedFieldStorage()) # Remove the form data from the Request class pagemaker.req.vars[method] = IndexedFieldStorage() + if 'files' in pagemaker.req.vars: + pagemaker.req.vars['files'] = {} return pagemaker def wrapper(*args, **kwargs): diff --git a/uweb3/request.py b/uweb3/request.py index 9247a419..7e3a9cdf 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -16,6 +16,7 @@ # uWeb modules from . import response +MAX_COOKIE_LENGTH = 4096 class CookieTooBigError(Exception): """Error class for cookie when size is bigger than 4096 bytes""" @@ -57,6 +58,7 @@ def __init__(self, env, registry): self._out_headers = [] self._out_status = 200 self._response = None + self.charset = "utf-8" self.method = self.env['REQUEST_METHOD'] self.vars = { 'cookie': { @@ -65,7 +67,7 @@ def __init__(self, env, registry): }, 'get': QueryArgsDict(parse_qs(self.env['QUERY_STRING'])), } - self.env['host'] = self.headers.get('Host', '') + self.env['host'] = self.headers.get('Host', '').strip().lower() if self.method in ('POST', 'PUT', 'DELETE'): request_body_size = 0 try: @@ -74,10 +76,66 @@ def __init__(self, env, registry): pass request_payload = self.env['wsgi.input'].read(request_body_size) self.input = request_payload - if self.env.get('CONTENT_TYPE', '') == 'application/json': - self.vars[self.method.lower()] = json.loads(request_payload) + self.env['mimetype'] = self.env.get('CONTENT_TYPE', '').split(';')[0] + + if self.env['mimetype'] == 'application/json': + try: + self.vars[self.method.lower()] = json.loads(request_payload) + except json.JSONDecodeError: + pass + elif self.env['mimetype'] == 'multipart/form-data': + boundary = self.env.get('CONTENT_TYPE', '').split(';')[1].strip().split('=')[1] + request_payload = request_payload.split(b'--%s' % boundary.encode(self.charset)) + self.vars['files'] = {} + fields = [] + for item in request_payload: + item = item.lstrip() + if item.startswith(b'Content-Disposition: form-data'): + nl = 0 + prevnl = 0 + itemlength = len(item) + name = filename = ContentType = charset = None + while nl < itemlength: + nl = item.index(b"\n", prevnl+len(b"\n")) + header = item[prevnl:nl] + prevnl = nl + if not header.strip(): + content = item[nl:].strip() + break + directives = header.strip().split(b';') + for directive in directives: + directive = directive.lstrip() + if directive.startswith(b'name='): + name = directive.split(b'=', 1)[1][1:-1].decode(self.charset) + if name == '_charset_': # default charset default case + self.charset = item[nl:].strip() + break + if directive.startswith(b'filename='): + filename = directive.split(b'=', 1)[1][1:-1].decode(self.charset) + if directive.startswith(b'Content-Type='): + ContentType = directive.split(b'=', 1)[1].decode(self.charset).split(";") + if len(ContentType) > 1: + if ContentType[1].startswith('charset'): + charset = ContentType[1].split('=')[1] + if ContentType[0].startswith('content-type'): + contenttype = ContentType[0].split(':')[1].strip() + if charset: + content = content.decode(charset) + elif not ContentType: + try: + content = content.decode(charset or self.charset) + except: + pass + if filename: + self.vars['files'][name] = {'filename': filename, + 'ContentType': ContentType, + 'content': content} + else: + fields.append('%s=%s' % (name, content)) + self.vars[self.method.lower()] = IndexedFieldStorage(stringIO.StringIO('&'.join(fields)), + environ={'REQUEST_METHOD': 'POST'}) else: - self.vars[self.method.lower()] = IndexedFieldStorage(stringIO.StringIO(request_payload.decode("utf-8")), + self.vars[self.method.lower()] = IndexedFieldStorage(stringIO.StringIO(request_payload.decode(self.charset)), environ={'REQUEST_METHOD': 'POST'}) @property @@ -140,13 +198,18 @@ def AddCookie(self, key, value, **attrs): When True, the cookie is only used for http(s) requests, and is not accessible through Javascript (DOM). """ - if isinstance(value, (str)) and len(value.encode('utf-8')) >= 4096: - raise CookieTooBigError("Cookie is larger than 4096 bytes and wont be set") + if isinstance(value, (str)) and len(value.encode('utf-8')) >= MAX_COOKIE_LENGTH: + raise CookieTooBigError("Cookie is larger than %d bytes and wont be set" % MAX_COOKIE_LENGTH) new_cookie = Cookie({key: value}) if 'max_age' in attrs: attrs['max-age'] = attrs.pop('max_age') new_cookie[key].update(attrs) + if 'samesite' not in attrs and 'secure' not in attrs: + try: # only supported from python 3.8 and up + attrs['samesite'] = 'Lax' # set default to LAX for no secure (eg, local) sessions. + except http.cookies.CookieError: + pass self.AddHeader('Set-Cookie', new_cookie[key].OutputString()) def AddHeader(self, name, value): @@ -191,8 +254,8 @@ def read_urlencoded(self): indexed = {} self.list = [] for field, value in parse_qsl(self.fp.read(self.length), - self.keep_blank_values, - self.strict_parsing): + self.keep_blank_values, + self.strict_parsing): if self.FIELD_AS_ARRAY.match(str(field)): field_group, field_key = self.FIELD_AS_ARRAY.match(field).groups() indexed.setdefault(field_group, cgi.MiniFieldStorage(field_group, {})) From 2083a68e24d04d87b98c53991bd3746f0d448be7 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 25 May 2021 10:41:54 +0200 Subject: [PATCH 108/118] add stef's Underdark address to the contributors file --- uweb3/libs/uploadlimiter.py | 46 +++++++++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 uweb3/libs/uploadlimiter.py diff --git a/uweb3/libs/uploadlimiter.py b/uweb3/libs/uploadlimiter.py new file mode 100644 index 00000000..ff94e124 --- /dev/null +++ b/uweb3/libs/uploadlimiter.py @@ -0,0 +1,46 @@ +#!/usr/bin/python3 +"""Module to validate uploaded files against some config vars if present""" + +__author__ = 'Jan Klopper ' +__version__ = '0.1' + +import magic + +class UploadLimiter: + def __init__(self, options=None, size=None, filetypes=None): + self.options = options + self.size = int(self.options.get('upload', {}).get('size', size) if options else size) + self.filetypes = None + filetypes = self.options.get('upload', {}).get('filetypes', filetypes) if options else filetypes + if filetypes: + filetypes = filetypes.lower().replace(' ', ',').split(',') + self.filetypes = [filetype.strip() for filetype in filetypes if filetype.strip()] + + def ValidFileType(self, content_type): + if content_type.lower() not in self.filetypes: + for filetype in self.filetypes: + if content_type.startswith(filetype): + return True + raise ContentTypeUploadException('%s is not an allowed file type.' % content_type) + return True + + def Validate(self, file): + if self.size and len(file) > self.size: + raise FilesizeUploadException('File is too big: %db > %db' % (len(file), self.size)) + + if self.filetypes: + content_type = magic.from_buffer(file, mime=True) + if not content_type: + content_type = 'text/plain' + return self.ValidFileType(content_type) + return True + + +class UploadException(Exception): + """There was an exception while uploading""" + +class FilesizeUploadException(UploadException): + """There was an exception while uploading due to filesize""" + +class ContentTypeUploadException(UploadException): + """There was an exception while uploading due to an invalid ContentType""" From 65d504323019b08c7accc34808b607c1a33899b3 Mon Sep 17 00:00:00 2001 From: Jan Date: Tue, 25 May 2021 10:47:21 +0200 Subject: [PATCH 109/118] fix typo as per PR9, thnx stef. --- CONTRIBUTORS | 2 +- uweb3/__init__.py | 20 ++++++++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/CONTRIBUTORS b/CONTRIBUTORS index 80f0abad..38253df4 100644 --- a/CONTRIBUTORS +++ b/CONTRIBUTORS @@ -3,7 +3,7 @@ uWeb was created by: - Jan Klopper - Arjen Pander - Elmer de Looff -- Stef van Houten +- Stef van Houten And has had code contributions from: diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 0f9fa766..ad41a25d 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -172,7 +172,7 @@ def __init__(self, page_class, routes, executing_path=None, config='config'): self.config = SettingsManager(filename=config, path=self.executing_path) self._accesslogger = None self._errorlogger = None - self.inital_pagemaker = page_class + self.initial_pagemaker = page_class self.registry = Registry() self.registry.logger = logging.getLogger('root') self.router = Router(page_class).router(routes) @@ -209,7 +209,7 @@ def __call__(self, env, start_response): # When we catch this error this means there is no method for the route in the currently selected pagemaker. # If this happens we default to the initial pagemaker because we don't know what the target pagemaker should be. # Then we set an internalservererror and move on - page_maker = self.inital_pagemaker + page_maker = self.initial_pagemaker try: # instantiate the pagemaker for this request pagemaker_instance = page_maker(req, @@ -333,7 +333,7 @@ def get_response(self, req, page_maker, method, args): # pylint: enable=W0212 return getattr(page_maker, method)(*args) except pagemaker.ReloadModules as message: - reload_message = reload(sys.modules[self.inital_pagemaker.__module__]) + reload_message = reload(sys.modules[self.initial_pagemaker.__module__]) return Response(content='%s\n%s' % (message, reload_message)) except ImmediateResponse as err: return err[0] @@ -359,8 +359,8 @@ def serve(self): print(f'Root dir is: {self.executing_path}') if hotreload: ignored_directories = ['__pycache__', - self.inital_pagemaker.PUBLIC_DIR, - self.inital_pagemaker.TEMPLATE_DIR] + self.initial_pagemaker.PUBLIC_DIR, + self.initial_pagemaker.TEMPLATE_DIR] ignored_extensions = [] interval = int(devconfig.get('checkinterval', 0)) if 'ignored_extensions' in devconfig: @@ -379,10 +379,10 @@ def serve(self): server.shutdown() def setup_routing(self): - if isinstance(self.inital_pagemaker, list): - routes = [route for route in self.inital_pagemaker[1:]] - self.inital_pagemaker[0].AddRoutes(tuple(routes)) - self.inital_pagemaker = self.inital_pagemaker[0] + if isinstance(self.initial_pagemaker, list): + routes = [route for route in self.initial_pagemaker[1:]] + self.initial_pagemaker[0].AddRoutes(tuple(routes)) + self.initial_pagemaker = self.initial_pagemaker[0] default_route = "routes" automatic_detection = True @@ -391,7 +391,7 @@ def setup_routing(self): automatic_detection = self.config.options['routing'].get('disable_automatic_route_detection', 'False') != 'True' if automatic_detection: - self.inital_pagemaker.LoadModules(routes=default_route) + self.initial_pagemaker.LoadModules(routes=default_route) class HotReload: From a90f7591c3c9ddecd5ed615229268cfbf51c0fe4 Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 28 May 2021 11:12:08 +0200 Subject: [PATCH 110/118] add requirements.txt to MANIFEST --- MANIFEST.in | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 932be389..aeadd4b5 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,3 +1,3 @@ -include README.md LICENSE DEVELOPMENT CONTRIBUTORS +include README.md LICENSE DEVELOPMENT CONTRIBUTORS requirements.txt recursive-include uweb3 *.html *.conf *.py recursive-include test *.py From cb78d5faad499770151b45d5e3f98495325cf522 Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 28 May 2021 11:12:53 +0200 Subject: [PATCH 111/118] move error_logging to log setion in config. --- uweb3/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index ad41a25d..13f46da5 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -187,7 +187,7 @@ def __init__(self, page_class, routes, executing_path=None, config='config'): accesslogging = self.config.options.get('log', {}).get('access_logging', True) != 'False' self._logrequest = self.logrequest if accesslogging else lambda *args: None # log exceptions even when development is present, but error_logging was not disabled specifically - errorlogging = self.config.options.get('development', {'error_logging': 'False'}).get('error_logging', 'True') == 'True' + errorlogging = self.config.options.get('log', {'error_logging': 'False'}).get('error_logging', 'True') == 'True' self._logerror = self.logerror if errorlogging else lambda *args: None def __call__(self, env, start_response): @@ -232,9 +232,7 @@ def __call__(self, env, start_response): executing_path=self.executing_path) response = pagemaker_instance.InternalServerError(*sys.exc_info()) - static = False - if method == 'Static': - static = True + static = (method == 'Static') if not static: if not isinstance(response, Response): From c717ca023f03430f8df8b3e6c9c926540a349064 Mon Sep 17 00:00:00 2001 From: Jan Date: Fri, 28 May 2021 11:22:34 +0200 Subject: [PATCH 112/118] remove logging from non debugging pagemaker, as already handled with log cofig setting higher up the chain --- uweb3/pagemaker/__init__.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index d13520ea..a3a762b8 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -285,7 +285,6 @@ def user(self): class BasePageMaker(Base): """Provides the base pagemaker methods for all the html generators.""" - _registery = [] # Default Static() handler cache durations, per MIMEtype, in days PUBLIC_DIR = 'static' @@ -435,8 +434,6 @@ def InternalServerError(self, exc_type, exc_value, traceback): """Returns a plain text notification about an internal server error.""" error = 'INTERNAL SERVER ERROR (HTTP 500) DURING PROCESSING OF %r' % ( self.req.path) - self.req.registry.logger.error( - error, exc_info=(exc_type, exc_value, traceback)) return response.Response( content=error, content_type='text/plain', httpcode=500) From 6a130e84dd7437736334f3897663b8b0400c4634 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 7 Jun 2021 20:34:16 +0200 Subject: [PATCH 113/118] remove unneeded registry class, add logger and errorlogger to request, add MAX size for file uploads/json payloads. --- uweb3/__init__.py | 9 ++------- uweb3/pagemaker/__init__.py | 11 +++++++---- uweb3/request.py | 10 ++++++---- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 13f46da5..a8de5ca6 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -39,9 +39,6 @@ class HTTPRequestException(HTTPException): class NoRouteError(Error): """The server does not know how to route this request""" -class Registry: - """Something to hook stuff to""" - class Router: def __init__(self, page_class): @@ -173,8 +170,6 @@ def __init__(self, page_class, routes, executing_path=None, config='config'): self._accesslogger = None self._errorlogger = None self.initial_pagemaker = page_class - self.registry = Registry() - self.registry.logger = logging.getLogger('root') self.router = Router(page_class).router(routes) self.setup_routing() self.encoders = { @@ -195,7 +190,7 @@ def __call__(self, env, start_response): Accepts the WSGI `environment` dictionary and a function to start the response and returns a response iterator. """ - req = request.Request(env, self.registry) + req = request.Request(env, self.logger, self.errorlogger) req.env['REAL_REMOTE_ADDR'] = request.return_real_remote_addr(req.env) response = None method = '_NotFound' @@ -284,7 +279,7 @@ def logger(self): def errorlogger(self): if not self._errorlogger: logger = logging.getLogger('uweb3_exception_logger') - logger.setLevel(logging.INFO) + logger.setLevel(logging.ERROR) logpath = os.path.join(self.executing_path, self.config.options.get('log', {}).get('exception_log', 'uweb3_exceptions.log')) delay = self.config.options.get('log', {}).get('exception_log_delay', False) != False encoding = self.config.options.get('log', {}).get('exception_log_encoding', None) diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index a3a762b8..774c026e 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -288,8 +288,11 @@ class BasePageMaker(Base): # Default Static() handler cache durations, per MIMEtype, in days PUBLIC_DIR = 'static' - CACHE_DURATION = MimeTypeDict({'text': 7, 'image': 30, 'application': 7, - 'text/css': 7}) + CACHE_DURATION = MimeTypeDict( + {'text': 7, + 'image': 30, + 'application': 7, + 'text/css': 7}) def __init__(self, req, @@ -505,7 +508,7 @@ def _SourceLines(filename, line_num, context=3): def InternalServerError(self, exc_type, exc_value, traceback): """Returns a HTTP 500 response with detailed failure analysis.""" - self.req.registry.logger.error( + self.req.errorlogger.error( 'INTERNAL SERVER ERROR (HTTP 500) DURING PROCESSING OF %r', self.req.path, exc_info=(exc_type, exc_value, traceback)) exception_data = { @@ -524,7 +527,7 @@ def InternalServerError(self, exc_type, exc_value, traceback): error_template.Parse(**exception_data), httpcode=500) except Exception: exc_type, exc_value, traceback = sys.exc_info() - self.req.registry.logger.critical( + self.req.errorlogger.criticals( 'INTERNAL SERVER ERROR (HTTP 500) DURING PROCESSING OF ERROR PAGE', exc_info=(exc_type, exc_value, traceback)) exception_data['error_for_error'] = True diff --git a/uweb3/request.py b/uweb3/request.py index 7e3a9cdf..5112ccf0 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -17,6 +17,7 @@ from . import response MAX_COOKIE_LENGTH = 4096 +MAX_REQUEST_BODY_SIZE = 20000000 #20MB class CookieTooBigError(Exception): """Error class for cookie when size is bigger than 4096 bytes""" @@ -51,10 +52,9 @@ def _BaseCookie__set(self, key, real_value, coded_value): class Request: - def __init__(self, env, registry): + def __init__(self, env, logger, errorlogger): self.env = env self.headers = dict(self.headers_from_env(env)) - self.registry = registry self._out_headers = [] self._out_status = 200 self._response = None @@ -68,20 +68,22 @@ def __init__(self, env, registry): 'get': QueryArgsDict(parse_qs(self.env['QUERY_STRING'])), } self.env['host'] = self.headers.get('Host', '').strip().lower() + self.logger = logger + self.errorlogger = errorlogger if self.method in ('POST', 'PUT', 'DELETE'): request_body_size = 0 try: request_body_size = int(self.env.get('CONTENT_LENGTH', 0)) except Exception: pass - request_payload = self.env['wsgi.input'].read(request_body_size) + request_payload = self.env['wsgi.input'].read(min(request_body_size, MAX_REQUEST_BODY_SIZE)) self.input = request_payload self.env['mimetype'] = self.env.get('CONTENT_TYPE', '').split(';')[0] if self.env['mimetype'] == 'application/json': try: self.vars[self.method.lower()] = json.loads(request_payload) - except json.JSONDecodeError: + except (json.JSONDecodeError, ValueError): pass elif self.env['mimetype'] == 'multipart/form-data': boundary = self.env.get('CONTENT_TYPE', '').split(';')[1].strip().split('=')[1] From 1c967a8a16b77ca364a462e99c00888eaa6c4cbb Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 26 Jul 2021 13:11:07 +0200 Subject: [PATCH 114/118] set the header for non parse json api calls --- uweb3/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index a8de5ca6..4fd0e395 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -231,10 +231,12 @@ def __call__(self, env, start_response): if not static: if not isinstance(response, Response): - # print('Upgrade response to Response class: %s' % type(response)) req.response.text = response response = req.response + if req.headers.get('uweb-noparse', None) == 'true': + response.content_type = 'application/json' + if not isinstance(response.text, Basesafestring): # make sure we always output Safe Strings for our known content-types encoder = self.encoders.get(response.clean_content_type(), self.encoders['default']) From 9d6d99a227f8ec0376f9676d1a4ac6216454ea55 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 26 Jul 2021 13:12:01 +0200 Subject: [PATCH 115/118] work towards a parser instance that knows about the noparse environment --- uweb3/pagemaker/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 774c026e..2c39c9f0 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -192,7 +192,9 @@ def parser(self): if '__parser' not in self.persistent: self.persistent.Set('__parser', templateparser.Parser( self.options.get('templates', {}).get('path', self.TEMPLATE_DIR))) - return self.persistent.Get('__parser') + parser = self.persistent.Get('__parser') + parser.noparse = (self.req.headers.get('uweb-noparse', None) == 'true') + return parser class WebsocketPageMaker(Base): @@ -286,8 +288,8 @@ def user(self): class BasePageMaker(Base): """Provides the base pagemaker methods for all the html generators.""" - # Default Static() handler cache durations, per MIMEtype, in days PUBLIC_DIR = 'static' + # Default Static() handler cache durations, per MIMEtype, in days CACHE_DURATION = MimeTypeDict( {'text': 7, 'image': 30, From cda2c574989301990955baad6d9adf5470439aa7 Mon Sep 17 00:00:00 2001 From: Jan Date: Mon, 26 Jul 2021 13:12:36 +0200 Subject: [PATCH 116/118] work towards a parser instance that knows about the noparse environment --- uweb3/templateparser.py | 49 +++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index be73fe8b..ea8c6a87 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -108,6 +108,7 @@ def values(self): """Returns a list with the values of the LazyTagValueRetrieval dict.""" return list(self.itervalues()) + EVALWHITELIST = { 'functions': {"abs": abs, "complex": complex, "min": min, "max": max, "pow": pow, "round": round, "len": len, "type": type, @@ -120,6 +121,7 @@ def values(self): ast.RShift, ast.Invert, ast.Call, ast.Name, ast.Compare, ast.Eq, ast.NotEq, ast.Not, ast.Or, ast.BoolOp, ast.Str)} + class Parser(dict): """A template parser that loads and caches templates and parses them by name. @@ -155,6 +157,8 @@ def __init__(self, path=None, templates=(), noparse=False, templateEncoding='utf % noparse: Bool ~~ False Skip parsing the templates to output, instead return their structure and replaced values + % templateEncoding: str ~~ utf-8 + Encoding of the template, used when reading the file. """ super().__init__() self.template_dir = path @@ -208,7 +212,7 @@ def AddTemplate(self, location, name=None): if self.template_dir: template_path = os.path.realpath(os.path.join(self.template_dir, location)) if os.path.commonprefix((template_path, self.template_dir)) != self.template_dir: - raise TemplateReadError('Could not load template %r, not in %r' % (template_path, self.template_dir)) + raise TemplateReadError('Could not load template %r, not in template dir' % template_path) else: template_path = location try: @@ -306,6 +310,12 @@ def RegisterTag(self, tag, value, persistent=False): @classmethod def JITTag(cls, function): + """Creates a JITTag instance of the given function + + Arguments: + % function: reference + Reference to the function + """ return JITTag(function) def ClearRequestTags(self): @@ -316,13 +326,27 @@ def ClearRequestTags(self): def SetTemplateEncoding(self, templateEncoding='utf-8'): """Allows the user to set the templateEncoding for this parser instance's templates. Any template reads, and reloads will be attempted with this - encoding.""" + encoding. + + Arguments: + % templateEncoding: str ~~ utf-8 + Encoding of the template, used when reading the file. + """ self.templateEncoding = templateEncoding - def SetEvalWhitelist(self, evalwhitelist=None): + def SetEvalWhitelist(self, evalwhitelist=None, append=False): """Allows the user to set the Eval Whitelist which limits the python operations allowed within this templateParsers Context. These are usually - triggered by If/Elif conditions and the like.""" + triggered by If/Elif conditions and the like. + + Arguments: + % evalwhitelist: Dict ~~ None + The new Dict of whitelisted eval AST items. + % append: bool ~~ False + When true, add the new items to the current list, else overwrite. + """ + if append: + evalwhitelist = EVALWHITELIST.update(evalwhitelist) self.astvisitor = AstVisitor(evalwhitelist) TemplateReadError = TemplateReadError @@ -412,19 +436,20 @@ def AddString(self, raw_template, filename=None): raise TemplateSyntaxError('Closed %d scopes too many in "%s"' % (abs(scope_diff), filename or raw_template)) raise TemplateSyntaxError('TemplateString left %d open scopes in "%s"' % (scope_diff, filename or raw_template)) - def Parse(self, returnRawTemplate=False, **kwds): + def Parse(self, **kwds): """Returns the parsed template as HTMLsafestring. The template is parsed by parsing each of its members and combining that. """ + noparse = False + if self.parser and self.parser.noparse: + noparse = True + htmlsafe = HTMLsafestring('').join(HTMLsafestring(tag.Parse(**kwds)) for tag in self) htmlsafe.content_hash = hashlib.md5(htmlsafe.encode()).hexdigest() - if returnRawTemplate: - raw = HTMLsafestring(self) - raw.content_hash = htmlsafe.content_hash - return raw - if self.parser and self.parser.noparse: + if noparse: + # Hash the page so that we can compare on the frontend if the html has changed htmlsafe.page_hash = hashlib.md5(HTMLsafestring(self).encode()).hexdigest() # Hashes the page and the content so we can know if we need to refresh the page on the frontend @@ -588,9 +613,9 @@ def Parse(self, **kwds): except TemplateFunctionError as error: raise TemplateFunctionError('%s in %s' % (error, self._template_path)) if self.parser and self.parser.noparse: - return {'template': self._templatepath[len(self.parser.template_dir):], + return {'template': self._template_path[len(self.parser.template_dir):], 'replacements': result.tags, - 'content_hash':result.content_hash, + 'content_hash': result.content_hash, 'page_hash': result.page_hash} return result From b355e73c92791d2b00bcb51a12436875b73514d1 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 9 Feb 2022 18:26:12 +0100 Subject: [PATCH 117/118] add sparse output pagemaker, JitTags with acces to the scope, various tests, Pin down requirements and remove some socket functionality since its not working atm. --- README.md | 8 -- requirements.txt | 7 +- test/test_templateparser.py | 131 ++++++++++++++++++++++++++++++ uweb3/__init__.py | 15 ++-- uweb3/connections.py | 2 +- uweb3/pagemaker/__init__.py | 33 +++++++- uweb3/pagemaker/sparserenderer.js | 131 ++++++++++++++++++++++++++++++ uweb3/request.py | 2 + uweb3/sockets.py | 38 --------- uweb3/templateparser.py | 74 ++++++++++------- 10 files changed, 353 insertions(+), 88 deletions(-) create mode 100644 uweb3/pagemaker/sparserenderer.js delete mode 100644 uweb3/sockets.py diff --git a/README.md b/README.md index cd896bf6..9c00c233 100644 --- a/README.md +++ b/README.md @@ -69,14 +69,6 @@ database = 'dbname' ``` To access your database connection simply use the connection attribute in any class that inherits from PageMaker. -# Config settings -If you are working on µWeb3 core make sure to enable the following setting in the config: -``` -[development] -dev = True -``` -This makes sure that µWeb3 restarts every time you modify something in the core of the framework aswell. - # Routing The default way to create new routes in µWeb3 is to create a folder called routes. In the routes folder create your pagemaker class of choice, the name doesn't matter as long as it inherits from PageMaker. diff --git a/requirements.txt b/requirements.txt index a8adceee..3c23c9a2 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ -PyMySQL -pytz -python-magic +PyMySQL==1.0.2 +pytz==2021.3 +python-magic==0.4.24 +python-socketio==5.5.1 diff --git a/test/test_templateparser.py b/test/test_templateparser.py index b12903bf..88a11e9c 100755 --- a/test/test_templateparser.py +++ b/test/test_templateparser.py @@ -985,5 +985,136 @@ def testReplaceTemplateWithDirectory(self): self.assertEqual(self.parser[self.simple].Parse(), self.simple_raw) +class DictTemplateTagBasic(unittest.TestCase): + """Tests validity and parsing of simple tags with dict output.""" + def setUp(self): + """Makes the Template class available on the instance.""" + self.tmpl = templateparser.Template + + def testTaglessTemplate(self): + """[BasicTag] Templates without tags get returned verbatim as SafeString""" + template = 'Template without any tags' + output = {'tags': {}, 'templatecontent': template} + self.assertEqual(self.tmpl(template, dictoutput=True).Parse(), output) + + def testSingleTagTemplate(self): + """[BasicTag] Templates with basic tags get returned proper""" + template = 'Template with [single] tag' + output = {'tags': { + '[single]': 'just one' + }, + 'templatecontent': template} + result = self.tmpl(template, dictoutput=True).Parse(single='just one') + self.assertEqual(result, output) + + def testSaveTagTemplate(self): + """[BasicTag] Templates with basic tags get returned properly when replacement is already html safe""" + template = 'Template with just [single] tag' + output = {'tags': { + '[single]': 'a safe' + }, + 'templatecontent': template} + result = self.tmpl(template, dictoutput=True).Parse(single=templateparser.HTMLsafestring('a safe')) + self.assertEqual(result, output) + + def testUnsaveTagTemplate(self): + """[BasicTag] Templates with basic tags get returned properly when replacement is not html safe""" + template = 'Template with just [single] tag' + output = {'tags': { + '[single]': '<b>an unsafe</b>' + }, + 'templatecontent': template} + result = self.tmpl(template, dictoutput=True).Parse(single='an unsafe') + self.assertEqual(result, output) + + def testCasedTag(self): + """[BasicTag] Tag names are case-sensitive""" + template = 'The parser has no trouble with [cAsE] [case].' + output = {'tags': { + '[cAsE]': 'mixed', + '[case]': '[case]' + }, + 'templatecontent': template} + + result = self.tmpl(template, dictoutput=True).Parse(cAsE='mixed') + self.assertEqual(result, output) + + def testUnderscoredTag(self): + """[BasicTag] Tag names may contain underscores""" + template = 'The template may contain [under_scored] tags.' + output = {'tags': { + '[under_scored]': 'underscored' + }, + 'templatecontent': template} + result = self.tmpl(template, dictoutput=True).Parse(under_scored='underscored') + self.assertEqual(result, output) + + def testMultiTagTemplate(self): + """[BasicTag] Multiple instances of a tag will all be replaced""" + template = '[adjective] [noun] are better than other [noun].' + output = {'tags': { + '[noun]': 'cows', + '[adjective]': 'Beefy' + }, + 'templatecontent': template} + result = self.tmpl(template, dictoutput=True).Parse(noun='cows', adjective='Beefy') + self.assertEqual(result, output) + + def testEmptyOrWhitespace(self): + """[BasicTag] Empty tags or tags containing whitespace aren't actual tags""" + template = 'This [is a] broken [] template, really' + output = {'tags': {}, + 'templatecontent': template} + result = self.tmpl(template, dictoutput=True).Parse(**{'is a': 'HORRIBLY', '': ', NASTY'}) + self.assertEqual(result, output) + + def testBadCharacterTags(self): + """[BasicTag] Tags containing bad characters are not considered tags""" + bad_chars = """ :~!@#$%^&*()+-={}\|;':",./<>? """ + template = ''.join('[%s] [check]' % char for char in bad_chars) + tags = {'[check]': '..'} + replaces = {char: 'FAIL' for char in bad_chars} + replaces['check'] = '..' + output = {'tags': tags, + 'templatecontent': template} + self.assertEqual(self.tmpl(template, dictoutput=True).Parse(**replaces), output) + + def testUnreplacedTag(self): + """[BasicTag] Template tags without replacement are returned verbatim""" + template = 'Template with an [undefined] tag.' + output = {'tags': {}, + 'templatecontent': template} + self.assertEqual(self.tmpl(template, dictoutput=True).Parse(), output) + + def testUnreplacedTag(self): + """[BasicTag] Access to private members is not allowed""" + template = 'Template with an [private.__class__] tag.' + output = {'tags': {}, + 'templatecontent': template} + self.assertEqual(self.tmpl(template, dictoutput=True).Parse(), output) + + def testBracketsInsideTag(self): + """[BasicTag] Innermost bracket pair are the tag's delimiters""" + template = 'Template tags may not contain [[spam][eggs]].' + expected = 'Template tags may not contain [opening or closing brackets].' + result = self.tmpl(template, dictoutput=True).Parse( + **{'[spam': 'EPIC', 'eggs]': 'FAIL', 'spam][eggs': 'EPIC FAIL', + 'spam': 'opening or ', 'eggs': 'closing brackets'}) + output = {'tags': { + '[spam]': 'opening or ', + '[eggs]': 'closing brackets' + }, + 'templatecontent': template} + self.assertEqual(output, result) + + def testTemplateInterpolationSyntax(self): + """[BasicTag] Templates support string interpolation of dicts""" + template = 'Hello [name]' + output = {'tags': { + '[name]': 'Bob', + }, + 'templatecontent': template} + self.assertEqual(self.tmpl(template, dictoutput=True) % {'name': 'Bob'}, output) + if __name__ == '__main__': unittest.main(testRunner=unittest.TextTestRunner(verbosity=2)) diff --git a/uweb3/__init__.py b/uweb3/__init__.py index 4fd0e395..e4db38c7 100644 --- a/uweb3/__init__.py +++ b/uweb3/__init__.py @@ -20,7 +20,7 @@ # Package classes from .response import Response, Redirect -from .pagemaker import PageMaker, decorators, WebsocketPageMaker, DebuggingPageMaker, LoginMixin +from .pagemaker import PageMaker, decorators, WebsocketPageMaker, DebuggingPageMaker, LoginMixin, SparseAsyncPages from .model import SettingsManager from .libs.safestring import HTMLsafestring, JSONsafestring, JsonEncoder, Basesafestring @@ -212,9 +212,14 @@ def __call__(self, env, start_response): executing_path=self.executing_path) # specifically call _PreRequest as promised in documentation if hasattr(pagemaker_instance, '_PreRequest'): - pagemaker_instance = pagemaker_instance._PreRequest() or pagemaker_instance - - response = self.get_response(req, pagemaker_instance, method, args) + try: + # we handle the preRequest seperately because otherwise we cannot show debugging info + pagemaker_instance = pagemaker_instance._PreRequest() or pagemaker_instance + except Exception: + # lets use the intended pagemaker, but skip prerequest as it crashes, this enabled rich debugging info if enabled. + response = pagemaker_instance.InternalServerError(*sys.exc_info()) + if not response: + response = self.get_response(req, pagemaker_instance, method, args) except Exception: # something broke in our pagemaker_instance, lets fall back to the most basic pagemaker for error output if hasattr(pagemaker_instance, '_ConnectionRollback'): @@ -234,7 +239,7 @@ def __call__(self, env, start_response): req.response.text = response response = req.response - if req.headers.get('uweb-noparse', None) == 'true': + if req.noparse: response.content_type = 'application/json' if not isinstance(response.text, Basesafestring): diff --git a/uweb3/connections.py b/uweb3/connections.py index 5e4a6851..1895a8c0 100644 --- a/uweb3/connections.py +++ b/uweb3/connections.py @@ -134,7 +134,7 @@ def request(self): except (KeyError, AttributeError, ValueError): pass requestdepth = requestdepth + 1 - raise TypeError('No request could be found in call Stack.') + raise TypeError('No request could be found in call Stack or no "model" connections are present.') def __enter__(self): """Proxies the transaction to the underlying relevant connection.""" diff --git a/uweb3/pagemaker/__init__.py b/uweb3/pagemaker/__init__.py index 2c39c9f0..c0c71a29 100644 --- a/uweb3/pagemaker/__init__.py +++ b/uweb3/pagemaker/__init__.py @@ -193,7 +193,7 @@ def parser(self): self.persistent.Set('__parser', templateparser.Parser( self.options.get('templates', {}).get('path', self.TEMPLATE_DIR))) parser = self.persistent.Get('__parser') - parser.noparse = (self.req.headers.get('uweb-noparse', None) == 'true') + parser.dictoutput = self.req.noparse return parser @@ -437,6 +437,9 @@ def _NotFound(self, _path): def InternalServerError(self, exc_type, exc_value, traceback): """Returns a plain text notification about an internal server error.""" + self.req.errorlogger.error( + 'INTERNAL SERVER ERROR (HTTP 500) DURING PROCESSING OF %r', + self.req.path, exc_info=(exc_type, exc_value, traceback)) error = 'INTERNAL SERVER ERROR (HTTP 500) DURING PROCESSING OF %r' % ( self.req.path) return response.Response( @@ -529,7 +532,7 @@ def InternalServerError(self, exc_type, exc_value, traceback): error_template.Parse(**exception_data), httpcode=500) except Exception: exc_type, exc_value, traceback = sys.exc_info() - self.req.errorlogger.criticals( + self.req.errorlogger.error( 'INTERNAL SERVER ERROR (HTTP 500) DURING PROCESSING OF ERROR PAGE', exc_info=(exc_type, exc_value, traceback)) exception_data['error_for_error'] = True @@ -603,6 +606,32 @@ def _CSPheaders(self): "%s %s" % (key, ' '.join(value)) for key, value in self._csp.items()) self.req.AddHeader('Content-Security-Policy', csp) + +class SparseAsyncPages(BasePageMaker): + """This mixin provides the template download functionality for client side + parsing based on the sparse json output functionality in the templateparser. + + The client is to download the template, (and cache them themselves, and do + replacements on their own based on the sparse page output they received. + """ + def SparseTemplateProvider(self, content_hash, path): + """This provides the client with the raw template as used by the server side + templateparser.""" + try: + template = self.parser[path] + except IOError: + return response.Response("This template does not exists", + content_type='text/plain', httpcode=404) + if template._template_hash != content_hash: + return response.Response("This template does not exists anymore. %s %s" % (template._template_hash, content_hash), + content_type='text/plain', httpcode=404) + return response.Response(template, content_type='text/plain') + + def SparseRenderedProvider(self): + return response.Response(templateparser.FileTemplate(os.path.join( + os.path.dirname(__file__), 'sparserenderer.js')), + content_type='application/javascript') + # ############################################################################## # Classes for public use (wildcard import) # diff --git a/uweb3/pagemaker/sparserenderer.js b/uweb3/pagemaker/sparserenderer.js new file mode 100644 index 00000000..0db5a46a --- /dev/null +++ b/uweb3/pagemaker/sparserenderer.js @@ -0,0 +1,131 @@ +"use strict" +var uweb_sparserenderer = window.uweb_sparserenderer || { + verbose: true, + templatecache: true, + templates: {}, + HandlePageLoad: function(event){ + if (this.verbose){ + console.log('Attaching event handlers to GET/POST actions for: ', window.location); + } + document.querySelectorAll('a').forEach(function(link) { + if(link.href){ + link.addEventListener('click', this.HandleClickEvent.bind(this)); + } + }, this); + document.querySelectorAll('form').forEach(function(link) { + link.addEventListener('submit', this.HandleSubmitEvent.bind(this)); + }, this); + }, + + HandleHistoryPop: function(event){ + if (this.verbose){ + console.log('History event triggered: ', event.state); + } + this.DoClick(event.state); + }, + + HandleClickEvent: function(event){ + if (this.verbose){ + console.log('User Link event registered on: ', event.target.href); + } + event.preventDefault(); + if(event.target.href){ // ignore links without + this.DoClick(event.target.href); + } + return false; + }, + + DoClick: async function (path){ + // Fetches the next URL from uweb while telling it we can do the parsing of the template locally. + let error; + let response = await fetch(path, { + headers: {'Accept': 'application/json'} + }) + .then(response => response.json()) + .then(result => {this.HandleUrlResponse(result, path)}) + .catch((error) => { + console.error('Error:', error); + }); + }, + + HandleSubmitEvent: function(event){ + if (this.verbose){ + console.log('User Form event registered on: ', event.target.action); + } + event.preventDefault(); + if(event.target.action){ + this.DoSubmit(event.target); + } + return false; + }, + + DoSubmit: async function (form){ + // Fetches the next URL from uweb while telling it we can do the parsing of the template locally. + let error; + const formData = new FormData(form); + const data = [...formData.entries()]; + console.log(formData, data); + const PostString = data + .map(x => `${encodeURIComponent(x[0])}=${encodeURIComponent(x[1])}`) + .join('&'); + if (this.verbose){ + console.log('User Form submit data: ', PostString); + } + if (new Array('head', 'get').indexOf(form.method.toLowerCase()) != -1){ + return this.DoClick(form.action + '?' + PostString); + } + let response = await fetch(form.action, { + method: form.method, + headers: {'Accept': 'application/json'}, + body: PostString + }) + .then(response => response.json()) + .then(result => {this.HandleUrlResponse(result, form.action)}) + .catch((error) => { + console.error('Error:', error); + }); + }, + + HandleUrlResponse: async function (response, path){ + window.history.pushState(path, "", path); + let template = await this.GetTemplate(response['template'], response['template_hash']); + Object.keys(response['replacements']).forEach(key => { + template = template.replaceAll(key, response['replacements'][key]); + } + ); + + document.body.innerHTML = template; + // re-init all handlers + this.HandlePageLoad(); + return template; + }, + + GetTemplate: async function (path, hash){ + let url = '/template/'+hash+path; + let error; + if (this.templatecache > 0 && + this.templates[path]){ + if (this.verbose){ + console.log('Cached template hit for '+path+' Saved '+this.templates[path].length+' bytes'); + } + return this.templates[path]; + } + return fetch(url)//, + //{integrity: "sha256-"+hash}) + .then(response => response.text()) + .then(data => { + this.templates[path] = data; + return data; + }) + .catch((error) => { + console.error('Error:', error); + }); + }, + + Load: function(){ + window.addEventListener('load', this.HandlePageLoad.bind(this)); + window.addEventListener('popstate', this.HandleHistoryPop.bind(this)); + } + + +}.Load(); diff --git a/uweb3/request.py b/uweb3/request.py index 5112ccf0..d61c704e 100644 --- a/uweb3/request.py +++ b/uweb3/request.py @@ -70,6 +70,8 @@ def __init__(self, env, logger, errorlogger): self.env['host'] = self.headers.get('Host', '').strip().lower() self.logger = logger self.errorlogger = errorlogger + self.noparse = self.headers.get('accept', '').lower() == 'application/json' + if self.method in ('POST', 'PUT', 'DELETE'): request_body_size = 0 try: diff --git a/uweb3/sockets.py b/uweb3/sockets.py deleted file mode 100644 index fc4c0280..00000000 --- a/uweb3/sockets.py +++ /dev/null @@ -1,38 +0,0 @@ -import os -import sys - -import socketio -import eventlet - -from uweb3 import uWeb, HotReload -from uweb3.helpers import StaticMiddleware - - -class SocketMiddleWare(socketio.WSGIApp): - def __init__(self, socketio_server, uweb3_server, socketio_path='socket.io'): - super(SocketMiddleWare, self).__init__(socketio_server, - uweb3_server, - socketio_path=socketio_path) - -class Uweb3SocketIO: - def __init__(self, app, sio, static_dir=None): - if not isinstance(app, uWeb): - raise Exception("App must be an uWeb3 instance!") - if not static_dir: - static_dir = os.path.dirname(os.path.abspath(__file__)) - - self.host = app.config.options['development'].get('host', '127.0.0.1') - self.port = app.config.options['development'].get('port', 8000) - if app.config.options['development'].get('dev', False) == 'True': - hotreload = app.config.options['development'].get('uweb_dev', 'False') - HotReload(app.executing_path, uweb_dev=hotreload) - self.setup_app(app, sio, static_dir) - - - def setup_app(self, app, sio, static_dir): - path = os.path.join(app.executing_path, 'static') - app = SocketMiddleWare(sio, app) - static_directory = [os.path.join(sys.path[0], path)] - app = StaticMiddleware(app, static_root='static', - static_dirs=static_directory) - eventlet.wsgi.server(eventlet.listen((self.host, int(self.port))), app) diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index ea8c6a87..f9ffab71 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -144,7 +144,7 @@ class Parser(dict): providing the `RegisterFunction` method to add or replace functions in this module constant. """ - def __init__(self, path=None, templates=(), noparse=False, templateEncoding='utf-8'): + def __init__(self, path=None, templates=(), dictoutput=False, templateEncoding='utf-8'): """Initializes a Parser instance. This sets up the template directory and preloads any templates given. @@ -154,20 +154,19 @@ def __init__(self, path=None, templates=(), noparse=False, templateEncoding='utf Search path for loading templates using AddTemplate(). % templates: iter of str ~~ None Names of templates to preload. - % noparse: Bool ~~ False + % dictoutput: Bool ~~ False Skip parsing the templates to output, instead return their - structure and replaced values + structure and replaced values as a dict % templateEncoding: str ~~ utf-8 Encoding of the template, used when reading the file. """ super().__init__() self.template_dir = path - self.noparse = noparse + self.dictoutput = dictoutput self.tags = {} self.requesttags = {} self.astvisitor = AstVisitor(EVALWHITELIST) self.templateEncoding = templateEncoding - for template in templates: self.AddTemplate(template) @@ -365,7 +364,7 @@ class Template(list): \]) # end of tag""", re.VERBOSE) - def __init__(self, raw_template, parser=None): + def __init__(self, raw_template, parser=None, dictoutput=False): """Initializes a Template from a string. Arguments: @@ -374,11 +373,18 @@ def __init__(self, raw_template, parser=None): % parser: Parser ~~ None An optional parser instance that is necessary to enable support for adding files to the current template. This is used by {{ inline }}. + % dictoutput: Bool ~~ False + An optional parser parameter which can be used to ask the parser to + output a dictionary of the template and its replaced vars, will only be + used when parser is None. + """ super().__init__() self.parser = parser + self.dictoutput = dictoutput self.scopes = [self] self.AddString(raw_template) + self.name = None def __eq__(self, other): """Returns the equality to another Template. @@ -411,6 +417,7 @@ def AddFile(self, name): TemplateReadError: The template file could not be read by the Parser. TypeError: There is no parser associated with the template. """ + self.name = name if self.parser is None: raise TypeError('The template requires parser for adding template files.') return self._AddToOpenScope(self.parser[name]) @@ -441,28 +448,23 @@ def Parse(self, **kwds): The template is parsed by parsing each of its members and combining that. """ - noparse = False - if self.parser and self.parser.noparse: - noparse = True - - htmlsafe = HTMLsafestring('').join(HTMLsafestring(tag.Parse(**kwds)) for tag in self) - htmlsafe.content_hash = hashlib.md5(htmlsafe.encode()).hexdigest() - - if noparse: - - # Hash the page so that we can compare on the frontend if the html has changed - htmlsafe.page_hash = hashlib.md5(HTMLsafestring(self).encode()).hexdigest() - # Hashes the page and the content so we can know if we need to refresh the page on the frontend - htmlsafe.tags = {} + dictoutput = self.parser and self.parser.dictoutput or self.dictoutput + if dictoutput: + output = {'tags': {}} + if self.name: + output['template'] = self.name + else: + output['templatecontent'] = str(self) for tag in self: if isinstance(tag, TemplateConditional): for flattend_branch in list(itertools.chain(*tag.branches)): for branch_tag in flattend_branch: if isinstance(branch_tag, TemplateTag): - htmlsafe.tags[str(branch_tag)] = branch_tag.Parse(**kwds) + output['tags'][str(branch_tag)] = branch_tag.Parse(**kwds) if isinstance(tag, TemplateTag): - htmlsafe.tags[str(tag)] = tag.Parse(**kwds) - return htmlsafe + output['tags'][str(tag)] = tag.Parse(**kwds) + return output + return HTMLsafestring('').join(HTMLsafestring(tag.Parse(**kwds)) for tag in self) @classmethod def TagSplit(cls, template): @@ -595,9 +597,9 @@ def __init__(self, template_path, parser=None, encoding='utf-8'): try: self._file_name = os.path.abspath(template_path) self._file_mtime = os.path.getmtime(self._file_name) - # self.parser can be None in which case we default to utf-8 with open(self._file_name, encoding=self.templateEncoding) as templatefile: raw_template = templatefile.read() + self._template_hash = HashContent(raw_template) super().__init__(raw_template, parser=parser) except (IOError, OSError) as error: raise TemplateReadError('Cannot open: %r %r' % (template_path, error)) @@ -612,11 +614,12 @@ def Parse(self, **kwds): result = super().Parse(**kwds) except TemplateFunctionError as error: raise TemplateFunctionError('%s in %s' % (error, self._template_path)) - if self.parser and self.parser.noparse: + if self.parser and self.parser.dictoutput: return {'template': self._template_path[len(self.parser.template_dir):], - 'replacements': result.tags, - 'content_hash': result.content_hash, - 'page_hash': result.page_hash} + 'replacements': result['tags'], + 'template_hash': self._template_hash}#, + # 'content_hash': result.content_hash, + # 'page_hash': result.page_hash} return result def ReloadIfModified(self): @@ -772,7 +775,7 @@ def Expression(tags, **kwds): """Checks the presence of all tags named on the branch.""" try: for tag in tags: - tag.GetValue(kwds) + f return False except (TemplateKeyError, TemplateNameError): return True @@ -924,7 +927,7 @@ def GetValue(self, replacements): for index in self.indices: value = self._GetIndex(value, index) if isinstance(value, JITTag): - return value() + return value(**replacements) return value except KeyError: raise TemplateNameError('No replacement with name %r' % self.name) @@ -1068,10 +1071,13 @@ def __init__(self, function): self.result = None # cache for results self.called = False # keep score of result cache usage, None and False might be correct results in the cache - def __call__(self): + def __call__(self, *args, **kwargs): """Returns the output of the earlier wrapped function""" if not self.called: - self.result = self.wrapped() + try: + self.result = self.wrapped(*args, **kwargs) + except TypeError: # the lambda does not expect params + self.result = self.wrapped() self.called = True return self.result @@ -1109,12 +1115,18 @@ def visit_Call(self, call): raise TemplateEvaluationError('`%s` is not an allowed function call' % call.func.id) def LimitedEval(expr, astvisitor, evallocals = {}): + """A limited Eval function which only allows certain operations""" tree = ast.parse(expr, mode='eval') astvisitor.visit(tree) return eval(compile(tree, "", "eval"), astvisitor.whitelists['functions'], evallocals) +def HashContent(string): + """Helper function for hashing of template files, this is needed to allow + users to download raw templates on their own which they know the hash for.""" + return hashlib.sha256(string.encode('utf-8')).hexdigest() + TAG_FUNCTIONS = { 'default': lambda d: HTMLsafestring('') + d, From ac277d1c71100962448296d98c61ce97f46eefa8 Mon Sep 17 00:00:00 2001 From: Jan Date: Wed, 6 Apr 2022 11:20:38 +0200 Subject: [PATCH 118/118] allow the user to always override registered tags from withing the pagemaker --- uweb3/templateparser.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/uweb3/templateparser.py b/uweb3/templateparser.py index f9ffab71..55c904a9 100644 --- a/uweb3/templateparser.py +++ b/uweb3/templateparser.py @@ -234,11 +234,11 @@ def Parse(self, template, **replacements): Returns: str: The template with relevant tags replaced by the replacement dict. """ - if self.tags: - replacements.update(self.tags) - if self.requesttags: - replacements.update(self.requesttags) - return self[template].Parse(**replacements) + output = {} + output.update(self.tags) + output.update(self.requesttags) + output.update(replacements) + return self[template].Parse(**output) def ParseString(self, template, **replacements): """Returns the given `template` with its tags replaced by **replacements. @@ -250,15 +250,14 @@ def ParseString(self, template, **replacements): @ replacements: dict Dictionary of replacement objects. Tags are looked up in here. - Returns: str: template with replaced tags. """ - if self.tags: - replacements.update(self.tags) - if self.requesttags: - replacements.update(self.requesttags) - return Template(template, parser=self).Parse(**replacements) + output = {} + output.update(self.tags) + output.update(self.requesttags) + output.update(replacements) + return Template(template, parser=self).Parse(**output) @staticmethod def RegisterFunction(name, function):