Skip to content

Commit

Permalink
Merge branch '4.0' of https://github.com/circulon/masonite into fix/e…
Browse files Browse the repository at this point in the history
…rrors_always_empty
  • Loading branch information
circulon committed Aug 22, 2024
2 parents d8075c8 + 80c738b commit 8e12258
Show file tree
Hide file tree
Showing 9 changed files with 159 additions and 71 deletions.
8 changes: 4 additions & 4 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@ argon2-cffi
bcrypt>=3.2,<3.3
black
cleo>=0.8.1,<0.9
cryptography>=3.3.1,<4.0
dotty_dict>=1.3.0<1.40
cryptography>=36,<37
dotty_dict>=1.3.0,<1.40
exceptionite>=2.0,<3
hashids>=1.3,<1.4
hfilesize>=0.1
Expand All @@ -20,7 +20,7 @@ python-dotenv>=0.15,<0.16
responses
slackblocks
tldextract>=2.2,<2.3
werkzeug>=2<3
watchdog>=2<3
werkzeug>=2,<3
watchdog>=2,<3
whitenoise>=5.2,<5.3
pyjwt>=2.3,<2.5
117 changes: 77 additions & 40 deletions src/masonite/cache/drivers/RedisDriver.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
from typing import Any, TYPE_CHECKING

import pendulum as pdlm
if TYPE_CHECKING:
from redis import Redis

Expand All @@ -9,59 +10,61 @@ class RedisDriver:
def __init__(self, application):
self.application = application
self.connection = None
self._internal_cache: "dict|None" = None
self.options = {}
self._internal_cache: dict = None

def set_options(self, options: dict) -> "RedisDriver":
self.options = options
return self

def get_connection(self) -> "Redis":
if self.connection:
return self.connection

try:
from redis import Redis
except ImportError:
raise ModuleNotFoundError(
"Could not find the 'redis' library. Run 'pip install redis' to fix this."
)

if not self.connection:
self.connection = Redis(
**self.options.get("options", {}),
host=self.options.get("host"),
port=self.options.get("port"),
password=self.options.get("password"),
decode_responses=True,
)

# populate the internal cache the first time
# the connection is established
if self._internal_cache is None and self.connection:
self._load_from_store(self.connection)
self.connection = Redis(
**self.options.get("options", {}),
host=self.options.get("host"),
port=self.options.get("port"),
password=self.options.get("password"),
decode_responses=True,
)

return self.connection

def _load_from_store(self, connection: "Redis" = None) -> None:
def _load_from_store(self) -> None:
"""
copy all the "cache" key value pairs for faster access
"""
if not connection:
if self._internal_cache is not None:
return

if self._internal_cache is None:
self._internal_cache = {}
self._internal_cache = {}

cursor = "0"
prefix = self.get_cache_namespace()
while cursor != 0:
cursor, keys = connection.scan(
cursor, keys = self.get_connection().scan(
cursor=cursor, match=prefix + "*", count=100000
)
if keys:
values = connection.mget(*keys)
values = self.get_connection().mget(*keys)
store_data = dict(zip(keys, values))
for key, value in store_data.items():
key = key.replace(prefix, "")
value = self.unpack_value(value)
self._internal_cache.setdefault(key, value)
# we dont load the ttl (expiry)
# because there is an O(N) performance hit
self._internal_cache[key] = {
"value": value,
"expires": None,
}

def get_cache_namespace(self) -> str:
"""
Expand All @@ -72,37 +75,66 @@ def get_cache_namespace(self) -> str:
return f"{namespace}cache:"

def add(self, key: str, value: Any = None) -> Any:
if not value:
if value is None:
return None

self.put(key, value)
return value

def get(self, key: str, default: Any = None, **options) -> Any:
if default and not self.has(key):
self.put(key, default, **options)
return default

return self._internal_cache.get(key)
self._load_from_store()
if not self.has(key):
return default or None

key_expiry = self._internal_cache[key].get("expires", None)
if key_expiry is None:
# the ttl value can also provide info on the
# existence of the key in the store
ttl = self.get_connection().ttl(key)
if ttl == -1:
# key exists but has no set ttl
ttl = self.get_default_timeout()
elif ttl == -2:
# key not found in store
self._internal_cache.pop(key)
return default or None

key_expiry = self._expires_from_ttl(ttl)
self._internal_cache[key]["expires"] = key_expiry

if pdlm.now() > key_expiry:
# the key has expired so remove it from the cache
self._internal_cache.pop(key)
return default or None

# the key has not yet expired
return self._internal_cache.get(key)["value"]

def put(self, key: str, value: Any = None, seconds: int = None, **options) -> Any:
if not key or value is None:
return None

time = self.get_expiration_time(seconds)

store_value = value
if isinstance(value, (dict, list, tuple)):
store_value = json.dumps(value)
elif isinstance(value, int):
store_value = str(value)

self._load_from_store()
key_ttl = seconds or self.get_default_timeout()
self.get_connection().set(
f"{self.get_cache_namespace()}{key}", store_value, ex=time
f"{self.get_cache_namespace()}{key}", store_value, ex=key_ttl
)

if not self.has(key):
self._internal_cache.update({key: value})
expires = self._expires_from_ttl(key_ttl)
self._internal_cache.update({
key: {
"value": value,
"expires": expires,
}
})

def has(self, key: str) -> bool:
self._load_from_store()
return key in self._internal_cache

def increment(self, key: str, amount: int = 1) -> int:
Expand All @@ -126,23 +158,28 @@ def remember(self, key: str, callable):
return self.get(key)

def forget(self, key: str) -> None:
if not self.has(key):
return
self.get_connection().delete(f"{self.get_cache_namespace()}{key}")
self._internal_cache.pop(key)

def flush(self) -> None:
return self.get_connection().flushall()
self.get_connection().flushall()
self._internal_cache = None

def get_expiration_time(self, seconds: int) -> int:
if seconds is None:
seconds = 31557600 * 10

return seconds
def get_default_timeout(self) -> int:
# if unset default timeout of cache vars is 1 month
return int(self.options.get("timeout", 60 * 60 * 24 * 30))

def unpack_value(self, value: Any) -> Any:
value = str(value)
if value.isdigit():
return str(value)
return int(value)

try:
return json.loads(value)
except json.decoder.JSONDecodeError:
return value

def _expires_from_ttl(self, ttl: int) -> pdlm.DateTime:
return pdlm.now().add(seconds=ttl)
43 changes: 26 additions & 17 deletions src/masonite/cookies/Cookie.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
from masonite.configuration import config


class Cookie:
def __init__(
self,
name,
value,
expires=None,
http_only=True,
path="/",
timezone=None,
secure=False,
samesite="Strict",
):
def __init__(self, name, value, **options):
self.options = options

self.name = name
self.value = value
self.http_only = http_only
self.secure = secure
self.expires = expires
self.timezone = timezone
self.samesite = samesite
self.path = path
self.http_only = self.__get_option('http_only', True)
self.secure = self.__get_option('secure', False)
self.expires = self.__get_option('expires', None)
self.timezone = self.__get_option('timezone', None)
self.samesite = self.__get_option('samesite', 'Strict')
self.path = self.__get_option('path', '/')
self.encrypt = self.__get_option('encrypt', True)

def render(self):
response = f"{self.name}={self.value};"
Expand All @@ -36,3 +32,16 @@ def render(self):
response += f"SameSite={self.samesite};"

return response

def __get_option(self, key: str, default: any):
"""
Get cookie options from config/session.py
if option key found in options then it return that
if not found in options then it will fetch from config
if not found in config then use the default value
"""
if key in self.options:
return self.options[key]
else:
cookie = config('session.drivers.cookie')
return cookie[key] if key in cookie else default
3 changes: 2 additions & 1 deletion src/masonite/drivers/queue/DatabaseDriver.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ def consume(self):
getattr(obj, callback)(*args)

except AttributeError:
obj(*args)
if callable(obj):
obj(*args)

self.success(
f"[{job['id']}][{pendulum.now(tz=self.options.get('tz', 'UTC')).to_datetime_string()}] Job Successfully Processed"
Expand Down
6 changes: 6 additions & 0 deletions src/masonite/middleware/route/EncryptCookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@
class EncryptCookies:
def before(self, request, response):
for _, cookie in request.cookie_jar.all().items():
if not cookie.encrypt:
continue

try:
cookie.value = request.app.make("sign").unsign(cookie.value)
except InvalidToken:
Expand All @@ -13,6 +16,9 @@ def before(self, request, response):

def after(self, request, response):
for _, cookie in response.cookie_jar.all().items():
if not cookie.encrypt:
continue

try:
cookie.value = request.app.make("sign").sign(cookie.value)
except InvalidToken:
Expand Down
12 changes: 9 additions & 3 deletions src/masonite/validation/Validator.py
Original file line number Diff line number Diff line change
Expand Up @@ -759,10 +759,16 @@ def passes(self, attribute, key, dictionary):
all_clear = False

if self.special != 0:
special_chars = "[^A-Za-z0-9]"
# custom specials are just a string of characters
# and may contain regex meta chars.
# so we search for them differently
if self.special_chars:
special_chars = f"[{self.special_chars}]"
if len(re.findall(special_chars, attribute)) < self.special:
special_count = sum(attribute.count(c) for c in self.special_chars)
else:
std_specials = "[^A-Za-z0-9]"
special_count = len(re.findall(std_specials, attribute))

if special_count < self.special:
self.special_check = False
all_clear = False

Expand Down
13 changes: 13 additions & 0 deletions tests/core/middleware/test_encrypt_cookies.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,16 @@ def test_encrypts_cookies(self):
response.cookie("test", "value")
EncryptCookies().after(request, response)
self.assertNotEqual(response.cookie("test"), "value")

def test_encrypt_cookies_opt_out(self):
request = self.make_request(
{"HTTP_COOKIE": f"test_key=test value"}
)

response = self.make_response()
EncryptCookies().before(request, None)
self.assertEqual(request.cookie("test_key", encrypt=False), "test value")

response.cookie("test", "value")
EncryptCookies().after(request, response)
self.assertNotEqual(response.cookie("test_key", encrypt=False), "test value")
10 changes: 7 additions & 3 deletions tests/features/cache/test_redis_cache.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def setUp(self):
self.application.make("cache")
self.driver = self.application.make("cache").store("redis")

def test_can_add_file_driver(self):
def test_can_add_redis_driver(self):
self.assertEqual(self.driver.add("add_key", "value"), "value")

def test_can_get_driver(self):
Expand All @@ -23,9 +23,13 @@ def test_can_increment(self):
self.driver.put("count", "1")
self.assertEqual(self.driver.get("count"), "1")
self.driver.increment("count")
self.assertEqual(self.driver.get("count"), "2")
self.assertEqual(self.driver.get("count"), 2)
self.driver.increment("count", 3)
self.assertEqual(self.driver.get("count"), 5)
self.driver.decrement("count")
self.assertEqual(self.driver.get("count"), "1")
self.assertEqual(self.driver.get("count"), 4)
self.driver.decrement("count", 2)
self.assertEqual(self.driver.get("count"), 2)

def test_will_not_get_expired(self):
self.driver.put("expire", "1", 1)
Expand Down
Loading

0 comments on commit 8e12258

Please sign in to comment.