1
0
mirror of https://github.com/typemill/typemill.git synced 2025-07-30 19:00:32 +02:00

Typemill Version 2 first milestone system area

This commit is contained in:
trendschau
2023-02-27 12:06:44 +01:00
parent 347bfbb476
commit 14565bd18e
27 changed files with 4793 additions and 0 deletions

1
.gitignore vendored
View File

@@ -18,6 +18,7 @@ content/01-cyanine-theme/01-colors-and-fonts.yaml
content/01-cyanine-theme/02-footer.yaml
content/01-cyanine-theme/03-content-elements.yaml
settings/settings.yaml
settings/license.yaml
settings/users
system/vendor
plugins/demo

7
system/author/css/a11y-dark.min.css vendored Normal file
View File

@@ -0,0 +1,7 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: a11y-dark
Author: @ericwbailey
Maintainer: @ericwbailey
Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css
*/.hljs{background:#2b2b2b;color:#f8f8f2}.hljs-comment,.hljs-quote{color:#d4d0ab}.hljs-deletion,.hljs-name,.hljs-regexp,.hljs-selector-class,.hljs-selector-id,.hljs-tag,.hljs-template-variable,.hljs-variable{color:#ffa07a}.hljs-built_in,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-type{color:#f5ab35}.hljs-attribute{color:gold}.hljs-addition,.hljs-bullet,.hljs-string,.hljs-symbol{color:#abe338}.hljs-section,.hljs-title{color:#00e0e0}.hljs-keyword,.hljs-selector-tag{color:#dcc6e0}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}@media screen and (-ms-high-contrast:active){.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-bullet,.hljs-comment,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-quote,.hljs-string,.hljs-symbol,.hljs-type{color:highlight}.hljs-keyword,.hljs-selector-tag{font-weight:700}}

709
system/author/js/highlight.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,335 @@
<?php
namespace Typemill\Controllers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Typemill\Models\ProcessImage;
use Typemill\Models\StorageWrapper;
# use Typemill\Models\ProcessFile;
# use Typemill\Models\Yaml;
# use Typemill\Controllers\ControllerAuthorBlockApi;
class ControllerApiImage extends ControllerData
{
# MISSING
#
# solution for logo
# return error messages and display in image component
# check if resized is bigger than original, then use original
# use original size checkbox
public function saveImage(Request $request, Response $response, $args)
{
$params = $request->getParsedBody();
if(!isset($params['image']) OR !isset($params['name']))
{
$response->getBody()->write(json_encode([
'message' => 'Image or name is missing.',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
$img = new ProcessImage();
if($this->settingActive('allowsvg'))
{
$img->addAllowedExtension('svg');
}
# prepare the image
if(!$img->prepareImage($params['image'], $params['name']))
{
$response->getBody()->write(json_encode([
'message' => $img->errors[0],
'fullerrors' => $img->errors,
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
# check if image name already exisits in live folder and create an unique name (do not overwrite existing files)
$storage = new StorageWrapper('\Typemill\Models\Storage');
$uniqueImageName = $storage->createUniqueImageName($img->getFilename(), $img->getExtension());
$img->setFilename($uniqueImageName);
# store the original image
if(!$img->storeOriginalToTmp())
{
$response->getBody()->write(json_encode([
'message' => $img->errors[0],
'fullerrors' => $img->errors,
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
# if image is not resizable (animated gif or svg)
if(!$img->isResizable())
{
if($img->saveOriginalForAll())
{
$response->getBody()->write(json_encode([
'message' => 'Image saved successfully',
'name' => 'media/live/' . $img->getFullName(),
]));
return $response->withHeader('Content-Type', 'application/json');
}
$response->getBody()->write(json_encode([
'message' => $img->errors[0],
'fullerrors' => $img->errors,
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
# for all other image types, check if they should be transformed to webp
if($this->settingActive('convertwebp'))
{
$img->setExtension('webp');
}
if(!$img->storeRenditionsToTmp($this->settings['images']))
{
$response->getBody()->write(json_encode([
'message' => $img->errors[0],
'fullerrors' => $img->errors,
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(500);
}
/*
if(isset($params['publish']) && $params['publish'])
{
if(!$img->publishImage($img->getFullName()))
{
$response->getBody()->write(json_encode([
'message' => $img->errors[0],
'fullerrors' => $img->errors,
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(500);
}
}
*/
$response->getBody()->write(json_encode([
'message' => 'Image saved successfully',
'name' => 'media/tmp/' . $img->getFullName(),
]));
return $response->withHeader('Content-Type', 'application/json');
}
public function publishImage(Request $request, Response $response, $args)
{
$params = $request->getParsedBody();
$imageProcessor = new ProcessImage($this->settings['images']);
if(!$imageProcessor->checkFolders())
{
return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
}
# check the resize modifier in the image markdown, set it to true and delete it from markdown
$noresize = false;
$markdown = isset($params['markdown']) ? $params['markdown'] : false;
if($markdown && (strlen($markdown) > 9) && (substr($markdown, -9) == '|noresize') )
{
$noresize = true;
$params['markdown'] = substr($markdown,0,-9);
}
if($imageProcessor->publishImage($noresize))
{
$request = $request->withParsedBody($params);
$block = new ControllerAuthorBlockApi($this->c);
if($params['new'])
{
return $block->addBlock($request, $response, $args);
}
return $block->updateBlock($request, $response, $args);
}
return $response->withJson(['errors' => 'could not store image to media folder'],500);
}
public function getMediaLibImages(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParsedBody();
$this->uri = $request->getUri()->withUserInfo('');
$imageProcessor = new ProcessImage($this->settings['images']);
if(!$imageProcessor->checkFolders('images'))
{
return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
}
$imagelist = $imageProcessor->scanMediaFlat();
$response->getBody()->write(json_encode([
'images' => $imagelist
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
public function getImage(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParsedBody();
$this->uri = $request->getUri()->withUserInfo('');
$this->setStructureDraft();
$imageProcessor = new ProcessImage($this->settings['images']);
if(!$imageProcessor->checkFolders('images'))
{
return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
}
$imageDetails = $imageProcessor->getImageDetails($this->params['name'], $this->structureDraft);
if($imageDetails)
{
return $response->withJson(['image' => $imageDetails]);
}
return $response->withJson(['errors' => 'Image not found or image name not valid.'], 404);
}
public function deleteImage(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParams();
$this->uri = $request->getUri()->withUserInfo('');
# minimum permission is that user is allowed to delete content
if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'delete'))
{
return $response->withJson(array('data' => false, 'errors' => 'You are not allowed to delete images.'), 403);
}
if(!isset($this->params['name']))
{
return $response->withJson(['errors' => 'image name is missing'],500);
}
$imageProcessor = new ProcessImage($this->settings['images']);
if(!$imageProcessor->checkFolders('images'))
{
return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
}
if($imageProcessor->deleteImage($this->params['name']))
{
return $response->withJson(['errors' => false]);
}
return $response->withJson(['errors' => 'Oops, looks like we could not delete all sizes of that image.'], 500);
}
public function saveVideoImage(Request $request, Response $response, $args)
{
/* get params from call */
$this->params = $request->getParams();
$this->uri = $request->getUri()->withUserInfo('');
$class = false;
$imageUrl = $this->params['markdown'];
if(strpos($imageUrl, 'https://www.youtube.com/watch?v=') !== false)
{
$videoID = str_replace('https://www.youtube.com/watch?v=', '', $imageUrl);
$videoID = strpos($videoID, '&') ? substr($videoID, 0, strpos($videoID, '&')) : $videoID;
$class = 'youtube';
}
if(strpos($imageUrl, 'https://youtu.be/') !== false)
{
$videoID = str_replace('https://youtu.be/', '', $imageUrl);
$videoID = strpos($videoID, '?') ? substr($videoID, 0, strpos($videoID, '?')) : $videoID;
$class = 'youtube';
}
if($class == 'youtube')
{
$videoURLmaxres = 'https://i1.ytimg.com/vi/' . $videoID . '/maxresdefault.jpg';
$videoURL0 = 'https://i1.ytimg.com/vi/' . $videoID . '/0.jpg';
}
$ctx = stream_context_create(array(
'https' => array(
'timeout' => 1
)
)
);
$imageData = @file_get_contents($videoURLmaxres, 0, $ctx);
if($imageData === false)
{
$imageData = @file_get_contents($videoURL0, 0, $ctx);
if($imageData === false)
{
return $response->withJson(array('errors' => 'could not get the video image'));
}
}
$imageData64 = 'data:image/jpeg;base64,' . base64_encode($imageData);
$desiredSizes = ['live' => ['width' => 560, 'height' => 315]];
$imageProcessor = new ProcessImage($this->settings['images']);
if(!$imageProcessor->checkFolders())
{
return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500);
}
$tmpImage = $imageProcessor->createImage($imageData64, $videoID, $desiredSizes);
if(!$tmpImage)
{
return $response->withJson(array('errors' => 'could not create temporary image'));
}
$imageUrl = $imageProcessor->publishImage();
if($imageUrl)
{
$this->params['markdown'] = '![' . $class . '-video](' . $imageUrl . ' "click to load video"){#' . $videoID. ' .' . $class . '}';
$request = $request->withParsedBody($this->params);
$block = new ControllerAuthorBlockApi($this->c);
if($this->params['new'])
{
return $block->addBlock($request, $response, $args);
}
return $block->updateBlock($request, $response, $args);
}
return $response->withJson(array('errors' => 'could not store the preview image'));
}
}

View File

@@ -0,0 +1,663 @@
<?php
namespace Typemill\Controllers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Typemill\Models\ProcessImage;
use Typemill\Models\ProcessFile;
use Typemill\Models\Yaml;
use Typemill\Controllers\ControllerAuthorBlockApi;
class ControllerApiMedia extends ControllerData
{
public function createImage(Request $request, Response $response, $args)
{
# get params from call
$params = $request->getParsedBody();
$imageProcessor = new ProcessImage($this->settings['images']);
if(!$imageProcessor->checkFolders('images'))
{
$response->getBody()->write(json_encode([
'message' => 'Please check if your media-folder exists and all folders inside are writable.'
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(403);
}
$imageParts = explode(";base64,", $params['image']);
$imageType = explode("/", $imageParts[0]);
if(!isset($imageType[1]))
{
$response->getBody()->write(json_encode([
'message' => 'We did not find an image type, the file might be corrupted.'
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(403);
}
$acceptedTypes = [
'png' => true,
'jpg' => true,
'jpeg' => true,
'gif' => true,
'webp' => true,
];
if(isset($this->settings['svg']) && $this->settings['svg'])
{
$acceptedTypes['svg+xml'] = true;
}
if(!isset($acceptedTypes[$imageType[1]]))
{
$response->getBody()->write(json_encode([
'message' => 'The image type is not supported.'
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(403);
}
$imageResult = $imageProcessor->createImage($params['image'], $params['name'], $this->settings['images']);
if($imageResult)
{
if(is_array($imageResult) && isset($imageResult['errors']))
{
return $response->withJson($imageResult,422);
}
# publish image directly, used for example by image field for meta-tabs
if($params['publish'])
{
$imageProcessor->publishImage();
}
return $response->withJson(['name' => 'media/live/' . $imageProcessor->getFullName(),'errors' => false]);
}
$response->getBody()->write(json_encode([
'message' => 'could not store image to temporary folder.'
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(403);
}
public function getMediaLibImages(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParsedBody();
$this->uri = $request->getUri()->withUserInfo('');
$imageProcessor = new ProcessImage($this->settings['images']);
if(!$imageProcessor->checkFolders('images'))
{
return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
}
$imagelist = $imageProcessor->scanMediaFlat();
$response->getBody()->write(json_encode([
'images' => $imagelist
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
public function getMediaLibFiles(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParsedBody();
$this->uri = $request->getUri()->withUserInfo('');
$fileProcessor = new ProcessFile();
if(!$fileProcessor->checkFolders())
{
return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
}
$filelist = $fileProcessor->scanFilesFlat();
$response->getBody()->write(json_encode([
'files' => $filelist
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
public function getImage(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParsedBody();
$this->uri = $request->getUri()->withUserInfo('');
$this->setStructureDraft();
$imageProcessor = new ProcessImage($this->settings['images']);
if(!$imageProcessor->checkFolders('images'))
{
return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
}
$imageDetails = $imageProcessor->getImageDetails($this->params['name'], $this->structureDraft);
if($imageDetails)
{
return $response->withJson(['image' => $imageDetails]);
}
return $response->withJson(['errors' => 'Image not found or image name not valid.'], 404);
}
public function getFile(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParams();
$this->uri = $request->getUri()->withUserInfo('');
$this->setStructureDraft();
$fileProcessor = new ProcessFile();
if(!$fileProcessor->checkFolders())
{
return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
}
$fileDetails = $fileProcessor->getFileDetails($this->params['name'], $this->structureDraft);
if($fileDetails)
{
return $response->withJson(['file' => $fileDetails]);
}
return $response->withJson(['errors' => 'file not found or file name invalid'],404);
}
public function getFileRestrictions(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParams();
$this->uri = $request->getUri()->withUserInfo('');
$restriction = 'all';
$userroles = $this->c->acl->getRoles();
if(isset($this->params['filename']) && $this->params['filename'] != '')
{
$writeYaml = new WriteYaml();
$restrictions = $writeYaml->getYaml('media' . DIRECTORY_SEPARATOR . 'files', 'filerestrictions.yaml');
if(isset($restrictions[$this->params['filename']]))
{
$restriction = $restrictions[$this->params['filename']];
}
}
return $response->withJson(['userroles' => $userroles, 'restriction' => $restriction]);
}
public function updateFileRestrictions(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParams();
$this->uri = $request->getUri()->withUserInfo('');
$filename = isset($this->params['filename']) ? $this->params['filename'] : false;
$role = isset($this->params['role']) ? $this->params['role'] : false;
if(!$filename OR !$role)
{
return $response->withJson(['errors' => ['message' => 'Filename or userrole is missing.']], 422);
}
$userroles = $this->c->acl->getRoles();
if($role != 'all' AND !in_array($role, $userroles))
{
return $response->withJson(['errors' => ['message' => 'Userrole is unknown.']], 422);
}
$writeYaml = new WriteYaml();
$restrictions = $writeYaml->getYaml('media' . DIRECTORY_SEPARATOR . 'files', 'filerestrictions.yaml');
if(!$restrictions)
{
$restrictions = [];
}
if($role == 'all')
{
unset($restrictions[$filename]);
}
else
{
$restrictions[$filename] = $role;
}
$writeYaml->updateYaml('media' . DIRECTORY_SEPARATOR . 'files', 'filerestrictions.yaml', $restrictions);
return $response->withJson(['restrictions' => $restrictions]);
}
public function uploadFile(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParams();
$this->uri = $request->getUri()->withUserInfo('');
if (!isset($this->params['file']))
{
return $response->withJson(['errors' => 'No file found.'],404);
}
$size = (int) (strlen(rtrim($this->params['file'], '=')) * 3 / 4);
$extension = pathinfo($this->params['name'], PATHINFO_EXTENSION);
$finfo = finfo_open( FILEINFO_MIME_TYPE );
$mtype = @finfo_file( $finfo, $this->params['file'] );
finfo_close($finfo);
if ($size === 0)
{
return $response->withJson(['errors' => 'File is empty.'],422);
}
# 20 MB (1 byte * 1024 * 1024 * 20 (for 20 MB))
if ($size > 20971520)
{
return $response->withJson(['errors' => 'File is bigger than 20MB.'],422);
}
# check extension first
if (!$this->checkAllowedExtensions($extension))
{
return $response->withJson(['errors' => 'File is not allowed.'],422);
}
# check mimetype and extension if there is a mimetype.
# in some environments the finfo_file does not work with a base64 string.
if($mtype)
{
if(!$this->checkAllowedMimeTypes($mtype, $extension))
{
return $response->withJson(['errors' => 'The mime-type or file extension is not allowed.'],422);
}
}
$fileProcessor = new ProcessFile();
if(!$fileProcessor->checkFolders())
{
return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
}
$fileinfo = $fileProcessor->storeFile($this->params['file'], $this->params['name']);
if($fileinfo)
{
# if the previous check of the mtype with the base64 string failed, then do it now again with the temporary file
if(!$mtype)
{
$filePath = str_replace('media/files', 'media/tmp', $fileinfo['url']);
$fullPath = $this->settings['rootPath'] . $filePath;
$finfo = finfo_open( FILEINFO_MIME_TYPE );
$mtype = @finfo_file( $finfo, $fullPath );
finfo_close($finfo);
if(!$mtype OR !$this->checkAllowedMimeTypes($mtype, $extension))
{
$fileProcessor->clearTempFolder();
return $response->withJson(['errors' => 'The mime-type is missing, not allowed or does not fit to the file extension.'],422);
}
}
# publish file directly, used for example by file field for meta-tabs
if(isset($this->params['publish']) && $this->params['publish'])
{
$fileProcessor->publishFile();
}
return $response->withJson(['errors' => false, 'info' => $fileinfo]);
}
return $response->withJson(['errors' => 'could not store file to temporary folder'],500);
}
public function publishImage(Request $request, Response $response, $args)
{
$params = $request->getParsedBody();
$imageProcessor = new ProcessImage($this->settings['images']);
if(!$imageProcessor->checkFolders())
{
return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
}
# check the resize modifier in the image markdown, set it to true and delete it from markdown
$noresize = false;
$markdown = isset($params['markdown']) ? $params['markdown'] : false;
if($markdown && (strlen($markdown) > 9) && (substr($markdown, -9) == '|noresize') )
{
$noresize = true;
$params['markdown'] = substr($markdown,0,-9);
}
if($imageProcessor->publishImage($noresize))
{
$request = $request->withParsedBody($params);
$block = new ControllerAuthorBlockApi($this->c);
if($params['new'])
{
return $block->addBlock($request, $response, $args);
}
return $block->updateBlock($request, $response, $args);
}
return $response->withJson(['errors' => 'could not store image to media folder'],500);
}
public function publishFile(Request $request, Response $response, $args)
{
$params = $request->getParsedBody();
$fileProcessor = new ProcessFile();
if(!$fileProcessor->checkFolders())
{
return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
}
if($fileProcessor->publishFile())
{
$request = $request->withParsedBody($params);
$block = new ControllerAuthorBlockApi($this->c);
if($params['new'])
{
return $block->addBlock($request, $response, $args);
}
return $block->updateBlock($request, $response, $args);
}
return $response->withJson(['errors' => 'could not store file to media folder'],500);
}
public function deleteImage(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParams();
$this->uri = $request->getUri()->withUserInfo('');
# minimum permission is that user is allowed to delete content
if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'delete'))
{
return $response->withJson(array('data' => false, 'errors' => 'You are not allowed to delete images.'), 403);
}
if(!isset($this->params['name']))
{
return $response->withJson(['errors' => 'image name is missing'],500);
}
$imageProcessor = new ProcessImage($this->settings['images']);
if(!$imageProcessor->checkFolders('images'))
{
return $response->withJson(['errors' => 'Please check if your media-folder exists and all folders inside are writable.'], 500);
}
if($imageProcessor->deleteImage($this->params['name']))
{
return $response->withJson(['errors' => false]);
}
return $response->withJson(['errors' => 'Oops, looks like we could not delete all sizes of that image.'], 500);
}
public function deleteFile(Request $request, Response $response, $args)
{
# get params from call
$this->params = $request->getParams();
$this->uri = $request->getUri()->withUserInfo('');
# minimum permission is that user is allowed to delete content
if(!$this->c->acl->isAllowed($_SESSION['role'], 'content', 'delete'))
{
return $response->withJson(array('data' => false, 'errors' => 'You are not allowed to delete files.'), 403);
}
if(!isset($this->params['name']))
{
return $response->withJson(['errors' => 'file name is missing'],500);
}
$fileProcessor = new ProcessFile();
if($fileProcessor->deleteFile($this->params['name']))
{
return $response->withJson(['errors' => false]);
}
return $response->withJson(['errors' => 'could not delete the file'],500);
}
public function saveVideoImage(Request $request, Response $response, $args)
{
/* get params from call */
$this->params = $request->getParams();
$this->uri = $request->getUri()->withUserInfo('');
$class = false;
$imageUrl = $this->params['markdown'];
if(strpos($imageUrl, 'https://www.youtube.com/watch?v=') !== false)
{
$videoID = str_replace('https://www.youtube.com/watch?v=', '', $imageUrl);
$videoID = strpos($videoID, '&') ? substr($videoID, 0, strpos($videoID, '&')) : $videoID;
$class = 'youtube';
}
if(strpos($imageUrl, 'https://youtu.be/') !== false)
{
$videoID = str_replace('https://youtu.be/', '', $imageUrl);
$videoID = strpos($videoID, '?') ? substr($videoID, 0, strpos($videoID, '?')) : $videoID;
$class = 'youtube';
}
if($class == 'youtube')
{
$videoURLmaxres = 'https://i1.ytimg.com/vi/' . $videoID . '/maxresdefault.jpg';
$videoURL0 = 'https://i1.ytimg.com/vi/' . $videoID . '/0.jpg';
}
$ctx = stream_context_create(array(
'https' => array(
'timeout' => 1
)
)
);
$imageData = @file_get_contents($videoURLmaxres, 0, $ctx);
if($imageData === false)
{
$imageData = @file_get_contents($videoURL0, 0, $ctx);
if($imageData === false)
{
return $response->withJson(array('errors' => 'could not get the video image'));
}
}
$imageData64 = 'data:image/jpeg;base64,' . base64_encode($imageData);
$desiredSizes = ['live' => ['width' => 560, 'height' => 315]];
$imageProcessor = new ProcessImage($this->settings['images']);
if(!$imageProcessor->checkFolders())
{
return $response->withJson(['errors' => ['message' => 'Please check if your media-folder exists and all folders inside are writable.']], 500);
}
$tmpImage = $imageProcessor->createImage($imageData64, $videoID, $desiredSizes);
if(!$tmpImage)
{
return $response->withJson(array('errors' => 'could not create temporary image'));
}
$imageUrl = $imageProcessor->publishImage();
if($imageUrl)
{
$this->params['markdown'] = '![' . $class . '-video](' . $imageUrl . ' "click to load video"){#' . $videoID. ' .' . $class . '}';
$request = $request->withParsedBody($this->params);
$block = new ControllerAuthorBlockApi($this->c);
if($this->params['new'])
{
return $block->addBlock($request, $response, $args);
}
return $block->updateBlock($request, $response, $args);
}
return $response->withJson(array('errors' => 'could not store the preview image'));
}
# https://www.sitepoint.com/mime-types-complete-list/
# https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types/Common_types
# https://wiki.selfhtml.org/wiki/MIME-Type/%C3%9Cbersicht
# http://www.mime-type.net/application/x-latex/
private function getAllowedMtypes()
{
return array(
'application/vnd.oasis.opendocument.chart' => 'odc',
'application/vnd.oasis.opendocument.formula' => 'odf',
'application/vnd.oasis.opendocument.graphics' => 'odg',
'application/vnd.oasis.opendocument.image' => 'odi',
'application/vnd.oasis.opendocument.presentation' => 'odp',
'application/vnd.oasis.opendocument.spreadsheet' => 'ods',
'application/vnd.oasis.opendocument.text' => 'odt',
'application/vnd.oasis.opendocument.text-master' => 'odm',
'application/powerpoint' => 'ppt',
'application/mspowerpoint' => ['ppt','ppz','pps','pot'],
'application/x-mspowerpoint' => 'ppt',
'application/vnd.ms-powerpoint' => 'ppt',
'application/vnd.openxmlformats-officedocument.presentationml.presentation' => 'pptx',
'application/x-visio' => ['vsd','vst','msw'],
'application/vnd.visio' => ['vsd','vst','msw'],
'application/x-project' => ['mpc','mpt','mpv','mpx'],
'application/vnd.ms-project' => 'mpp',
'application/excel' => ['xla','xlb','xlc','xld','xlk','xll','xlm','xls','xlt','xlv','xlw'],
'application/msexcel' => ['xls','xla'],
'application/x-excel' => ['xla','xlb','xlc','xld','xlk','xll','xlm','xls','xlt','xlv','xlw'],
'application/x-msexcel' => ['xls', 'xla','xlw'],
'application/vnd.ms-excel' => ['xlb','xlc','xll','xlm','xls','xlw'],
'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' => 'xlsx',
'application/mshelp' => ['hlp','chm'],
'application/msword' => ['doc','dot'],
'application/vnd.openxmlformats-officedocument.wordprocessingml.document' => 'docx',
'application/vnd.apple.keynote' => 'key',
'application/vnd.apple.numbers' => 'numbers',
'application/vnd.apple.pages' => 'pages',
'application/x-latex' => ['ltx','latex'],
'application/pdf' => 'pdf',
'application/vnd.amazon.mobi8-ebook' => 'azw3',
'application/x-mobipocket-ebook' => 'mobi',
'application/epub+zip' => 'epub',
'application/x-gtar' => 'gtar',
'application/x-tar' => 'tar',
'application/zip' => 'zip',
'application/gzip' => 'gz',
'application/x-gzip' => ['gz', 'gzip'],
'application/x-compressed' => ['gz','tgz','z','zip'],
'application/x-zip-compressed' => 'zip',
'application/vnd.rar' => 'rar',
'application/x-7z-compressed' => '7z',
'application/rtf' => 'rtf',
'application/x-rtf' => 'rtf',
'text/calendar' => 'ics',
'text/comma-separated-values' => 'csv',
'text/css' => 'css',
'text/plain' => 'txt',
'text/richtext' => 'rtx',
'text/rtf' => 'rtf',
'audio/basic' => ['au','snd'],
'audio/mpeg' => 'mp3',
'audio/mp4' => 'mp4',
'audio/ogg' => 'ogg',
'audio/wav' => 'wav',
'audio/x-aiff' => ['aif','aiff','aifc'],
'audio/x-midi' => ['mid','midi'],
'audio/x-mpeg' => 'mp2',
'audio/x-pn-realaudio' => ['ram','ra'],
'image/png' => 'png',
'image/jpeg' => ['jpeg','jpe','jpg'],
'image/gif' => 'gif',
'image/tiff' => ['tiff','tif'],
'image/svg+xml' => 'svg',
'image/x-icon' => 'ico',
'image/webp' => 'webp',
'video/mpeg' => ['mpeg','mpg','mpe'],
'video/mp4' => 'mp4',
'video/ogg' => ['ogg','ogv'],
'video/quicktime' => ['qt','mov'],
'video/vnd.vivo' => ['viv','vivo'],
'video/webm' => 'webm',
'video/x-msvideo' => 'avi',
'video/x-sgi-movie' => 'movie',
'video/3gpp' => '3gp',
);
}
protected function checkAllowedMimeTypes($mtype, $extension)
{
$allowedMimes = $this->getAllowedMtypes();
if(!isset($allowedMimes[$mtype]))
{
return false;
}
if(
(is_array($allowedMimes[$mtype]) && !in_array($extension, $allowedMimes[$mtype])) OR
(!is_array($allowedMimes[$mtype]) && $allowedMimes[$mtype] != $extension )
)
{
return false;
}
return true;
}
protected function checkAllowedExtensions($extension)
{
$mtypes = $this->getAllowedMtypes();
foreach($mtypes as $mtExtension)
{
if(is_array($mtExtension))
{
if(in_array($extension, $mtExtension))
{
return true;
}
}
else
{
if($extension == $mtExtension)
{
return true;
}
}
}
return false;
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace Typemill\Controllers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Typemill\Models\Validation;
use Typemill\Models\StorageWrapper;
use Typemill\Models\License;
class ControllerApiSystemLicense extends ControllerData
{
public function createLicense(Request $request, Response $response)
{
$params = $request->getParsedBody();
if(!isset($params['license']) OR !is_array($params['license']))
{
$response->getBody()->write(json_encode([
'message' => 'License data missing.',
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
# validate input
$validate = new Validation();
$validationresult = $validate->newLicense($params['license']);
if($validationresult !== true)
{
$response->getBody()->write(json_encode([
'message' => 'Please correct errors in form.',
'errors' => $validate->returnFirstValidationErrors($validationresult)
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
$license = new License();
$licensedata = $license->activateLicense($params['license']);
if(!$licensedata)
{
$response->getBody()->write(json_encode([
'message' => $license->getMessage()
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(400);
}
$response->getBody()->write(json_encode([
'message' => 'Licence has been stored',
'licensedata' => $license->getLicenseData($this->c->get('urlinfo'))
]));
return $response->withHeader('Content-Type', 'application/json')->withStatus(200);
}
}

View File

@@ -0,0 +1,117 @@
<?php
namespace Typemill\Controllers;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Message\ResponseInterface as Response;
use Slim\Routing\RouteContext;
use Typemill\Models\Validation;
use Typemill\Models\User;
class ControllerWebAuth extends Controller
{
public function show(Request $request, Response $response)
{
return $this->c->get('view')->render($response, 'auth/login.twig', [
#'captcha' => $this->checkIfAddCaptcha(),
]);
}
public function login(Request $request, Response $response)
{
if( ( null !== $request->getattribute('csrf_result') ) OR ( $request->getattribute('csrf_result') === false ) )
{
$this->c->flash->addMessage('error', 'The form has a timeout, please try again.');
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'));
}
$input = $request->getParsedBody();
$validation = new Validation();
$settings = $this->c->get('settings');
if($validation->signin($input))
{
$user = new User();
if(!$user->setUserWithPassword($input['username']))
{
# return error
}
$userdata = $user->getUserData();
if($userdata && password_verify($input['password'], $userdata['password']))
{
# check if user has confirmed the account
if(isset($userdata['optintoken']) && $userdata['optintoken'])
{
$this->c->get('flash')->addMessage('error', 'Your registration is not confirmed yet. Please check your e-mails and use the confirmation link.');
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
}
$user->login();
return $response->withHeader('Location', $this->routeParser->urlFor('settings.show'))->withStatus(302);
/*
# if user is allowed to view content-area
$acl = $this->c->get('acl');
if($acl->hasRole($userdata['userrole']) && $acl->isAllowed($userdata['userrole'], 'content', 'view'))
{
$editor = (isset($this->settings['editor']) && $this->settings['editor'] == 'visual') ? 'visual' : 'raw';
return $response->withHeader('Location', $this->routeParser->urlFor('content.' . $editor))->withStatus(302);
}
return $response->withHeader('Location', $this->routeParser->urlFor('user.account'))->withStatus(302);
*/
}
}
if(isset($this->settings['securitylog']) && $this->settings['securitylog'])
{
\Typemill\Static\Helpers::addLogEntry('wrong login');
}
$this->c->get('flash')->addMessage('error', 'Ups, wrong password or username, please try again.');
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
}
/**
* log out a user
*
* @param obj $request the slim request object
* @param obj $response the slim response object
* @return obje $response with redirect to route
*/
public function logout(Request $request, Response $response)
{
# check https://www.php.net/session_destroy
if(isset($_SESSION))
{
# Unset all of the session variables.
$_SESSION = array();
# If it's desired to kill the session, also delete the session cookie. This will destroy the session, and not just the session data!
if (ini_get("session.use_cookies"))
{
$params = session_get_cookie_params();
setcookie(
session_name(),
'',
time() - 42000,
$params["path"], $params["domain"],
$params["secure"], $params["httponly"]
);
}
# Finally, destroy the session.
session_destroy();
}
return $response->withHeader('Location', $this->routeParser->urlFor('auth.show'))->withStatus(302);
}
}

View File

@@ -0,0 +1,144 @@
<?php
namespace Typemill\Middleware;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Routing\RouteContext;
use Slim\Psr7\Response;
use Typemill\Models\User;
class ApiAuthentication
{
public function __invoke(Request $request, RequestHandler $handler)
{
$routeContext = RouteContext::fromRequest($request);
$baseURL = $routeContext->getBasePath();
# check if it is a session based authentication
if ($request->hasHeader('X-Session-Auth'))
{
session_start();
$authenticated = (
(isset($_SESSION['username'])) &&
(isset($_SESSION['login']))
)
? true : false;
if($authenticated)
{
# here we have to load userdata and pass them through request or response
$user = new User();
if($user->setUser($_SESSION['username']))
{
$userdata = $user->getUserData();
$request = $request->withAttribute('c_username', $userdata['username']);
$request = $request->withAttribute('c_userrole', $userdata['userrole']);
$response = $handler->handle($request);
return $response;
}
}
else
{
# return error message
}
}
# api authentication with basic auth
# inspired by tuupola
$host = $request->getUri()->getHost();
$scheme = $request->getUri()->getScheme();
$server_params = $request->getServerParams();
/*
# HTTP allowed only if secure is false or server is in relaxed array.
# use own logic for https proto forwarding
if($scheme !== "https" && $this->options["secure"] !== true)
{
$allowedHost = in_array($host, $this->options["relaxed"]);
# if 'headers' is in the 'relaxed' key, then we check for forwarding
$allowedForward = false;
if (in_array("headers", $this->options["relaxed"]))
{
if ( $request->getHeaderLine("X-Forwarded-Proto") === "https" && $request->getHeaderLine('X-Forwarded-Port') === "443")
{
$allowedForward = true;
}
}
if (!($allowedHost || $allowedForward))
{
$message = sprintf("Insecure use of middleware over %s denied by configuration.", strtoupper($scheme));
throw new \RuntimeException($message);
}
}
*/
$params = [];
if (preg_match("/Basic\s+(.*)$/i", $request->getHeaderLine("Authorization"), $matches))
{
$explodedCredential = explode(":", base64_decode($matches[1]), 2);
if (count($explodedCredential) == 2)
{
[$params["user"], $params["password"]] = $explodedCredential;
}
}
if(!empty($params))
{
# load userdata
$user = new User();
if($user->setUserWithPassword($params['user']))
{
$userdata = $user->getUserData();
# this might be unsecure, check for === comparator
$apiaccess = ( isset($userdata['apiaccess']) && $userdata['apiaccess'] == true ) ? true : false;
if($userdata && $apiaccess && password_verify($params['password'], $userdata['password']))
{
$request = $request->withAttribute('c_username', $userdata['username']);
$request = $request->withAttribute('c_userrole', $userdata['userrole']);
# this executes code from routes first and then executes middleware
$response = $handler->handle($request);
return $response;
}
else
{
# if basic auth is set but with wrong credentials
$response = new Response();
$response->getBody()->write(json_encode([
'message' => 'Authentication failed.'
]));
return $response->withHeader('WWW-Authenticate', 'Basic realm=')->withStatus(401);
}
}
}
# elseif ($request->getHeaderLine('X-Requested-With') === 'XMLHttpRequest') {
# if you use this, then all xhr-calls need a session.
# no direct xhr calls without session are possible
# might increase security, but can have unwanted cases e.g. when you
# want to provide public api accessible for all by javascript (do you ever want??)
# }
$response = new Response();
$response->getBody()->write('Zugriff nicht erlaubt.');
return $response->withStatus(401);
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Typemill\Middleware;
use Psr\Http\Server\MiddlewareInterface;
use Slim\Routing\RouteParser;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
class ApiAuthorization implements MiddlewareInterface
{
public function __construct($acl, string $resource = NULL, string $action = NULL)
{
$this->acl = $acl;
$this->resource = $resource;
$this->action = $action;
}
public function process(Request $request, RequestHandler $handler) :Response
{
if(!$this->acl->isAllowed($request->getAttribute('c_userrole'), $this->resource, $this->action))
{
$message = 'userrole: ' . $request->getAttribute('c_userrole') . ' resource: ' . $this->resource . ' action: ' . $this->action;
$response = new Response();
$response->getBody()->write(json_encode([
'message' => $message
]));
return $response->withStatus(401);
}
$response = $handler->handle($request);
return $response;
}
}

View File

@@ -0,0 +1,34 @@
<?php
namespace Typemill\Middleware;
use Psr\Http\Server\MiddlewareInterface;
use Slim\Routing\RouteParser;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
class WebAuthorization implements MiddlewareInterface
{
public function __construct(RouteParser $router, $acl, string $resource = NULL, string $action = NULL)
{
$this->router = $router;
$this->acl = $acl;
$this->resource = $resource;
$this->action = $action;
}
public function process(Request $request, RequestHandler $handler) :Response
{
if(!$this->acl->isAllowed($request->getAttribute('c_userrole'), $this->resource, $this->action))
{
$response = new Response();
return $response->withHeader('Location', $this->router->urlFor('user.account'))->withStatus(302);
}
$response = $handler->handle($request);
return $response;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Typemill\Middleware;
use Psr\Http\Server\MiddlewareInterface;
use Slim\Routing\RouteParser;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
class WebRedirectIfAuthenticated implements MiddlewareInterface
{
public function __construct(RouteParser $router, $settings)
{
$this->router = $router;
$this->settings = $settings;
}
public function process(Request $request, RequestHandler $handler) :Response
{
$authenticated = (
(isset($_SESSION['username'])) &&
(isset($_SESSION['login']))
)
? true : false;
if($authenticated)
{
$editor = (isset($this->settings['editor']) && $this->settings['editor'] == 'visual') ? 'visual' : 'raw';
$response = new Response();
return $response->withHeader('Location', $this->router->urlFor('content.' . $editor))->withStatus(302);
}
$response = $handler->handle($request);
return $response;
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace Typemill\Middleware;
use Psr\Http\Server\MiddlewareInterface;
use Slim\Routing\RouteParser;
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as RequestHandler;
use Slim\Psr7\Response;
use Typemill\Models\User;
class WebRedirectIfUnauthenticated implements MiddlewareInterface
{
public function __construct(RouteParser $router)
{
$this->router = $router;
}
public function process(Request $request, RequestHandler $handler) :response
{
# session authentication
if(
(isset($_SESSION['username'])) &&
(isset($_SESSION['login']))
)
{
# load userdata
$user = new User();
if($user->setUser($_SESSION['username']))
{
# pass username and userrole
$userdata = $user->getUserData();
$request = $request->withAttribute('c_username', $userdata['username']);
$request = $request->withAttribute('c_userrole', $userdata['userrole']);
# this executes code from routes first and then executes middleware
$response = $handler->handle($request);
return $response;
}
}
# this executes only middleware code and not code from route
$response = new Response();
return $response->withHeader('Location', $this->router->urlFor('auth.show'))->withStatus(302);
}
}

View File

@@ -0,0 +1,258 @@
<?php
namespace Typemill\Models;
use Typemill\Models\StorageWrapper;
class License
{
public $message = '';
private $plans = [
'44699' => [
'name' => 'MAKER',
'scope' => ['MAKER' => true]
],
'33334' => [
'name' => 'BUSINESS',
'scope' => ['MAKER' => true, 'BUSINESS' => true]
]
];
public function getMessage()
{
return $this->message;
}
# used for license management in admin settings
public function getLicenseData(array $urlinfo)
{
# returns data for settings page
$licensedata = $this->checkLicense();
if($licensedata)
{
$licensedata['plan'] = $this->plans[$licensedata['plan']]['name'];
$licensedata['domaincheck'] = $this->checkLicenseDomain($licensedata['domain'], $urlinfo);
$licensedata['datecheck'] = $this->checkLicenseDate($licensedata['payed_until']);
return $licensedata;
}
return false;
}
# used to activate or deactivate features that require a license
public function getLicenseScope(array $urlinfo)
{
$licensedata = $this->checkLicense();
if(!$licensedata)
{
return false;
}
$domain = $this->checkLicenseDomain($licensedata['domain'], $urlinfo);
$date = $this->checkLicenseDate($licensedata['payed_until']);
$domain = true;
if($domain && $date)
{
return $this->plans[$licensedata['plan']]['scope'];
}
return false;
}
public function refreshLicense()
{
}
# check the local licence file (like pem or pub)
private function checkLicense()
{
$storage = new StorageWrapper('\Typemill\Models\Storage');
$licensedata = $storage->getYaml('settings', 'license.yaml');
if(!$licensedata)
{
$this->message = 'no license found';
return false;
}
if(!isset($licensedata['license'],$licensedata['email'],$licensedata['domain'],$licensedata['plan'],$licensedata['payed_until'],$licensedata['signature']))
{
$this->message = 'License data incomplete';
return false;
}
$licenseStatus = $this->validateLicense($licensedata);
if($licenseStatus === true)
{
unset($licensedata['signature']);
# check here if payed until is in past
return $licensedata;
}
return false;
}
private function validateLicense($data)
{
$public_key_pem = $this->getPublicKeyPem();
$binary_signature = base64_decode($data['signature']);
$data['email'] = $this->hashMail($data['email']);
unset($data['signature']);
# test manipulate data
#$data['plan'] = 'wrong';
$data = json_encode($data);
# Check signature
$verified = openssl_verify($data, $binary_signature, $public_key_pem, OPENSSL_ALGO_SHA256);
if ($verified == 1)
{
return true;
}
elseif ($verified == 0)
{
$this->message = 'License data are invalid';
return false;
}
else
{
$this->message = 'There was an error checking the license signature';
return false;
}
}
public function activateLicense($params)
{
# prepare data for call to licence server
$licensedata = [
'license' => $params['license'],
'email' => $this->hashMail($params['email']),
'domain' => $params['domain']
];
$postdata = http_build_query($licensedata);
$authstring = $this->getPublicKeyPem();
$authstring = hash('sha256', substr($authstring, 0, 50));
$options = array (
'http' => array (
'method' => 'POST',
'ignore_errors' => true,
'header' => "Content-Type: application/x-www-form-urlencoded\r\n" .
"Accept: application/json\r\n" .
"Authorization: $authstring\r\n" .
"Connection: close\r\n",
'content' => $postdata
)
);
$context = stream_context_create($options);
$response = file_get_contents('https://service.typemill.net/api/v2/activate', false, $context);
if(substr($http_response_header[0], -6) != "200 OK")
{
$this->message = 'the license server responded with: ' . $http_response_header[0];
return false;
}
$signedLicense = json_decode($response,true);
if(isset($signedLicense['code']))
{
# $this->message = 'Something went wrong. Please check your input data or contact the support.';
$this->message = $signedLicense['code'];
return false;
}
/*
# check for positive and validate response data
if($signedLicense['license'])
{
$this->message = ;
}
*/
$signedLicense['license']['email'] = trim($params['email']);
$storage = new StorageWrapper('\Typemill\Models\Storage');
$storage->updateYaml('settings', 'license.yaml', $signedLicense['license']);
return true;
}
private function updateLicence()
{
# todo
}
private function checkLicenseDomain(string $licensedomain, array $urlinfo)
{
$licensehost = parse_url($licensedomain, PHP_URL_HOST);
$licensehost = str_replace("www.", "", $licensehost);
$thishost = parse_url($urlinfo['baseurl'], PHP_URL_HOST);
$thishost = str_replace("www.", "", $thishost);
$whitelist = ['localhost', '127.0.0.1', 'typemilltest.', $licensehost];
foreach($whitelist as $domain)
{
if(substr($thishost, 0, strlen($domain)) == $domain)
{
return true;
}
}
return false;
}
private function checkLicenseDate(string $payed_until)
{
if(strtotime($payed_until) > strtotime(date('Y-m-d')))
{
return true;
}
return false;
}
private function hashMail(string $mail)
{
return hash('sha256', trim($mail) . 'TYla5xa8JUur');
}
private function getPublicKeyPem()
{
$pkeyfile = getcwd() . DIRECTORY_SEPARATOR . 'settings' . DIRECTORY_SEPARATOR . "public_key.pem";
if(file_exists($pkeyfile) && is_readable($pkeyfile))
{
# fetch public key from file and ready it
$fp = fopen($pkeyfile, "r");
$public_key_pem = fread($fp, 8192);
fclose($fp);
return $public_key_pem;
}
return false;
}
}

View File

@@ -0,0 +1,243 @@
<?php
namespace Typemill\Models;
use Typemill\Models\Folder;
class ProcessAssets
{
# holds the path to the temporary image folder
public $basepath = false;
public $tmpFolder = false;
public $errors = [];
public function __construct()
{
ini_set('memory_limit', '512M');
$this->basepath = getcwd() . DIRECTORY_SEPARATOR;
$this->tmpFolder = $this->basepath . 'media' . DIRECTORY_SEPARATOR . 'tmp' . DIRECTORY_SEPARATOR;
}
public function clearTempFolder()
{
$files = scandir($this->tmpFolder);
$now = time();
$result = true;
foreach($files as $file)
{
if (!in_array($file, array(".","..")))
{
$filelink = $this->tmpFolder . $file;
if(file_exists($filelink))
{
$filetime = filemtime($filelink);
if($now - $filetime > 1800)
{
if(!unlink($filelink))
{
$result = false;
}
}
}
}
}
return $result;
}
/*
public function checkFolders($forassets = null)
{
$folders = [$this->mediaFolder, $this->tmpFolder, $this->fileFolder];
if($forassets == 'images')
{
$folders = [$this->mediaFolder, $this->tmpFolder, $this->originalFolder, $this->liveFolder, $this->thumbFolder, $this->customFolder];
}
foreach($folders as $folder)
{
if(!file_exists($folder) && !is_dir( $folder ))
{
if(!mkdir($folder, 0755, true))
{
return false;
}
if($folder == $this->thumbFolder)
{
# cleanup old systems
$this->cleanupLiveFolder();
# generate thumbnails from live folder
$this->generateThumbs();
}
}
elseif(!is_writeable($folder) OR !is_readable($folder))
{
return false;
}
# check if thumb-folder is empty, then generate thumbs from live folder
if($folder == $this->thumbFolder && $this->is_dir_empty($folder))
{
# cleanup old systems
$this->cleanupLiveFolder();
# generate thumbnails from live folder
$this->generateThumbs();
}
}
return true;
}
*/
public function is_dir_empty($dir)
{
return (count(scandir($dir)) == 2);
}
/*
public function setFileName($originalname, $type, $overwrite = NULL)
{
$pathinfo = pathinfo($originalname);
$this->extension = isset($pathinfo['extension']) ? strtolower($pathinfo['extension']) : null;
$this->filename = Folder::createSlug($pathinfo['filename']);
$filename = $this->filename;
# check if file name is
if(!$overwrite)
{
$suffix = 1;
$destination = $this->liveFolder;
if($type == 'file')
{
$destination = $this->fileFolder;
}
while(file_exists($destination . $filename . '.' . $this->extension))
{
$filename = $this->filename . '-' . $suffix;
$suffix++;
}
}
$this->filename = $filename;
return true;
}
*/
/*
public function getName()
{
return $this->filename;
}
public function setExtension($extension)
{
$this->extension = $extension;
}
public function getExtension()
{
return $this->extension;
}
public function getFullName()
{
return $this->filename . '.' . $this->extension;
}
*/
/*
public function cleanupLiveFolder()
{
# delete all old thumbs mlibrary in live folder
foreach(glob($this->liveFolder . '*mlibrary*') as $filename)
{
unlink($filename);
}
return true;
}
*/
public function findPagesWithUrl($structure, $url, $result)
{
foreach ($structure as $key => $item)
{
if($item->elementType == 'folder')
{
$result = $this->findPagesWithUrl($item->folderContent, $url, $result);
}
else
{
$live = getcwd() . DIRECTORY_SEPARATOR . 'content' . $item->pathWithoutType . '.md';
$draft = getcwd() . DIRECTORY_SEPARATOR . 'content' . $item->pathWithoutType . '.txt';
# check live first
if(file_exists($live))
{
$content = file_get_contents($live);
if (stripos($content, $url) !== false)
{
$result[] = $item->urlRelWoF;
}
# if not in live, check in draft
elseif(file_exists($draft))
{
$content = file_get_contents($draft);
if (stripos($content, $url) !== false)
{
$result[] = $item->urlRelWoF;
}
}
}
}
}
return $result;
}
public function formatSizeUnits($bytes)
{
if ($bytes >= 1073741824)
{
$bytes = number_format($bytes / 1073741824, 2) . ' GB';
}
elseif ($bytes >= 1048576)
{
$bytes = number_format($bytes / 1048576, 2) . ' MB';
}
elseif ($bytes >= 1024)
{
$bytes = number_format($bytes / 1024, 2) . ' KB';
}
elseif ($bytes > 1)
{
$bytes = $bytes . ' bytes';
}
elseif ($bytes == 1)
{
$bytes = $bytes . ' byte';
}
else
{
$bytes = '0 bytes';
}
return $bytes;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace Typemill\Models;
class Yaml extends StorageWrapper
{
/**
* Get the a yaml file.
* @param string $fileName is the name of the Yaml Folder.
* @param string $yamlFileName is the name of the Yaml File.
*/
public function getYaml($folderName, $yamlFileName)
{
$yaml = $this->getFile($folderName, $yamlFileName);
if($yaml)
{
return \Symfony\Component\Yaml\Yaml::parse($yaml);
}
return false;
}
/**
* Writes a yaml file.
* @param string $fileName is the name of the Yaml Folder.
* @param string $yamlFileName is the name of the Yaml File.
* @param array $contentArray is the content as an array.
*/
public function updateYaml($folderName, $yamlFileName, $contentArray)
{
$yaml = \Symfony\Component\Yaml\Yaml::dump($contentArray,6);
if($this->writeFile($folderName, $yamlFileName, $yaml))
{
return true;
}
return false;
}
}

View File

@@ -0,0 +1,585 @@
<?php
namespace Typemill\Models;
#use Slim\Http\UploadedFile;
use Typemill\Static\Slug;
class ProcessImage extends ProcessAssets
{
protected $imgstring = false;
protected $type = false;
protected $extension = false;
protected $allowedExtensions = ['png' => true, 'jpg' => true, 'jpeg' => true, 'webp' => true];
protected $filename = false;
protected $animated = false;
protected $resizable = true;
protected $sizes = [];
public function prepareImage($image, $name)
{
# change clear tmp folder and delete only old ones
$this->clearTempFolder();
#$this->checkFolders('image');
$this->decode($image);
$this->setPathInfo($name);
$this->checkAllowedExtension();
if(empty($this->errors))
{
return true;
}
return false;
}
public function storeOriginalToTmp()
{
# $this->saveName();
$this->saveOriginal();
if(empty($this->errors))
{
return true;
}
return false;
}
public function storeRenditionsToTmp($sizes)
{
# transform image-stream into image
$image = $this->createImage();
$originalsize = $this->getImageSize($image);
foreach($sizes as $destinationfolder => $desiredsize)
{
$desiredsize = $this->calculateSize($originalsize, $desiredsize);
$resizedImage = $this->resizeImage($image, $desiredsize, $originalsize);
$this->saveResizedImage($resizedImage, $destinationfolder, $this->extension);
imagedestroy($resizedImage);
}
imagedestroy($image);
if(empty($this->errors))
{
return true;
}
return false;
}
# decode a base64 image string from js image components
public function decode(string $image)
{
$imageParts = explode(";base64,", $image);
if(!isset($imageParts[0]) OR !isset($imageParts[1]))
{
$this->errors[] = 'Could not decode image, probably not a base64 encoding.';
return false;
}
$type = explode("/", $imageParts[0]);
$this->type = strtolower($type[0]);
$this->imgstring = base64_decode($imageParts[1]);
return true;
}
# set the pathinfo (name and extension) and slugify a unique name if option to overwrite existing files is false
public function setPathInfo(string $name)
{
$pathinfo = pathinfo($name);
if(!$pathinfo)
{
$this->errors[] = 'Could not read pathinfo.';
return false;
}
$this->extension = isset($pathinfo['extension']) ? strtolower($pathinfo['extension']) : false;
$this->filename = Slug::createSlug($pathinfo['filename']);
if(!$this->extension OR !$this->filename)
{
$this->errors[] = 'Extension or filename are missing.';
return false;
}
return true;
}
public function getExtension()
{
return $this->extension;
}
public function getFilename()
{
return $this->filename;
}
public function setFilename($filename)
{
$this->filename = $filename;
}
public function getFullName()
{
return $this->filename . '.' . $this->extension;
}
# add an allowed image extension like svg
public function addAllowedExtension(string $extension)
{
$this->allowedExtensions[$extension] = true;
}
# force an image type like webp
public function setExtension(string $extension)
{
$this->extension = $extension;
}
public function checkAllowedExtension()
{
if(!isset($this->allowedExtensions[$this->extension]))
{
$this->errors[] = 'Images with this extension are not allowed.';
return false;
}
return true;
}
# check if image should not be resized (animated gif and svg)
public function isResizable()
{
if($this->type == 'gif' && $this->detectAnimatedGif())
{
$this->resizable = false;
}
if($this->type == 'svg+xml')
{
$this->resizable = false;
}
return $this->resizable;
}
public function detectAnimatedGif()
{
$is_animated = preg_match('#(\x00\x21\xF9\x04.{4}\x00\x2C.*){2,}#s', $this->imgstring);
if ($is_animated == 1)
{
$this->animated = true;
}
return $this->animated;
}
# save the original image to temp folder
public function saveOriginal($destinationfolder = 'ORIGINAL')
{
$path = $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.' . $this->extension;
if(!file_put_contents($path, $this->imgstring))
{
$this->errors[] = 'could not store the image in the temporary folder';
}
}
# save the original image for all sizes/folders
public function saveOriginalForAll()
{
$this->saveOriginal('LIVE');
$this->saveOriginal('THUMBS');
}
public function createImage()
{
return imagecreatefromstring($this->imgstring);
}
public function getImageSize($image)
{
return ['width' => imagesx($image), 'height' => imagesy($image)];
}
public function calculateSize(array $originalsize, array $desiredsize)
{
# if desired size is bigger than the actual image, then drop the desired sizes and use the actual image size instead
if($desiredsize['width'] > $originalsize['width'])
{
return $originalsize;
}
if(!isset($desiredsize['height']))
{
$resizeFactor = $originalsize['width'] / $desiredsize['width'];
$desiredsize['height'] = round( ($originalsize['height'] / $resizeFactor), 0);
}
return $desiredsize;
}
public function resizeImage($image, array $desired, array $original)
{
# resize
$ratio = max($desired['width']/$original['width'], $desired['height']/$original['height']);
$h = $desired['height'] / $ratio;
$x = ($original['width'] - $desired['width'] / $ratio) / 2;
$y = ($original['height'] - $desired['height'] / $ratio) / 2;
$w = $desired['width'] / $ratio;
$resizedImage = imagecreatetruecolor($desired['width'], $desired['height']);
# preserve transparency
if($this->extension == "gif" or $this->extension == "png" or $this->extension == "webp")
{
imagecolortransparent($resizedImage, imagecolorallocatealpha($resizedImage, 0, 0, 0, 127));
imagealphablending($resizedImage, false);
imagesavealpha($resizedImage, true);
}
imagecopyresampled($resizedImage, $image, 0, 0, $x, $y, $desired['width'], $desired['height'], $w, $h);
return $resizedImage;
}
public function saveResizedImage($resizedImage, string $destinationfolder, string $extension)
{
$destinationfolder = strtoupper($destinationfolder);
switch($extension)
{
case "png":
$storedImage = imagepng( $resizedImage, $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.png', 9 );
break;
case "gif":
$storedImage = imagegif( $resizedImage, $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.gif' );
break;
case "webp":
$storedImage = imagewebp( $resizedImage, $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.webp', 80);
break;
case "jpg":
case "jpeg":
$storedImage = imagejpeg( $resizedImage, $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.' . $extension, 80);
break;
default:
$storedImage = false;
}
if(!$storedImage)
{
$failedImage = $this->tmpFolder . $destinationfolder . '+' . $this->filename . '.' . $extension;
$this->errors[] = "Could not store the resized version $failedImage";
return false;
}
return true;
}
# publish image function is moved to storage model
# MOVE TO STORAGE ??
public function deleteImage($name)
{
# validate name
$name = basename($name);
if(!file_exists($this->originalFolder . $name) OR !unlink($this->originalFolder . $name))
{
$this->errors[] = "We could not delete the original image";
}
if(!file_exists($this->liveFolder . $name) OR !unlink($this->liveFolder . $name))
{
$this->errors[] = "We could not delete the live image";
}
if(!file_exists($this->thumbFolder . $name) OR !unlink($this->thumbFolder . $name))
{
$this->errors[] = "we could not delete the thumb image";
}
# delete custom images (resized and grayscaled) array_map('unlink', glob("some/dir/*.txt"));
$pathinfo = pathinfo($name);
foreach(glob($this->customFolder . $pathinfo['filename'] . '\-*.' . $pathinfo['extension']) as $image)
{
# you could check if extension is the same here
if(!unlink($image))
{
$this->errors[] = "we could not delete a custom image (grayscale or resized)";
}
}
if(empty($this->errors))
{
return true;
}
return false;
}
# in use ??
public function deleteImageWithName($name)
{
# e.g. delete $name = 'logo...';
$name = basename($name);
if($name != '' && !in_array($name, array(".","..")))
{
foreach(glob($this->liveFolder . $name) as $file)
{
unlink($file);
}
foreach(glob($this->originalFolder . $name) as $file)
{
unlink($file);
}
foreach(glob($this->thumbFolder . $name) as $file)
{
unlink($file);
}
}
}
# in use ??
public function copyImage($name,$sourcefolder,$targetfolder)
{
copy($sourcefolder . $name, $targetfolder . $name);
}
/**
* Moves the uploaded file to the upload directory. Only used for settings / NON VUE.JS uploads
*
* @param string $directory directory to which the file is moved
* @param UploadedFile $uploadedFile file uploaded file to move
* @return string filename of moved file
*/
public function moveUploadedImage(UploadedFile $uploadedFile, $overwrite = false, $name = false, $folder = NULL)
{
$this->setFileName($uploadedFile->getClientFilename(), 'file');
if($name)
{
$this->setFileName($name . '.' . $this->extension, 'file', $overwrite);
}
if(!$folder)
{
$folder = $this->liveFolder;
}
$uploadedFile->moveTo($folder . $this->getFullName());
return $this->getFullName();
}
/*
# save the image name as txt to temp folder
public function saveName()
{
$path = $this->tmpFolder . $this->filename . '.txt';
if(!fopen($path, "w"))
{
$this->errors[] = 'could not store the filename in the temporary folder';
}
}
*/
/*
* scans content of a folder (without recursion)
* vars: folder path as string
* returns: one-dimensional array with names of folders and files
*/
public function scanMediaFlat()
{
$thumbs = array_diff(scandir($this->thumbFolder), array('..', '.'));
$imagelist = array();
foreach ($thumbs as $key => $name)
{
if (file_exists($this->liveFolder . $name))
{
$imagelist[] = [
'name' => $name,
'timestamp' => filemtime($this->liveFolder . $name),
'src_thumb' => 'media/thumbs/' . $name,
'src_live' => 'media/live/' . $name,
];
}
}
$imagelist = Helpers::array_sort($imagelist, 'timestamp', SORT_DESC);
return $imagelist;
}
# get details from existing image for media library
public function getImageDetails($name, $structure)
{
$name = basename($name);
if (!in_array($name, array(".","..")) && file_exists($this->liveFolder . $name))
{
$imageinfo = getimagesize($this->liveFolder . $name);
if(!$imageinfo && pathinfo($this->liveFolder . $name, PATHINFO_EXTENSION) == 'svg')
{
$imagedetails = [
'name' => $name,
'timestamp' => filemtime($this->liveFolder . $name),
'bytes' => filesize($this->liveFolder . $name),
'width' => '---',
'height' => '---',
'type' => 'svg',
'src_thumb' => 'media/thumbs/' . $name,
'src_live' => 'media/live/' . $name,
'pages' => $this->findPagesWithUrl($structure, $name, $result = [])
];
}
else
{
$imagedetails = [
'name' => $name,
'timestamp' => filemtime($this->liveFolder . $name),
'bytes' => filesize($this->liveFolder . $name),
'width' => $imageinfo[0],
'height' => $imageinfo[1],
'type' => $imageinfo['mime'],
'src_thumb' => 'media/thumbs/' . $name,
'src_live' => 'media/live/' . $name,
'pages' => $this->findPagesWithUrl($structure, $name, $result = [])
];
}
return $imagedetails;
}
return false;
}
public function generateThumbs()
{
# generate images from live folder to 'tmthumbs'
$liveImages = scandir($this->liveFolder);
$result = false;
foreach ($liveImages as $key => $name)
{
if (!in_array($name, array(".","..")))
{
$result = $this->generateThumbFromImageFile($name);
}
}
return $result;
}
public function generateThumbFromImageFile($filename)
{
$this->setFileName($filename, 'image', $overwrite = true);
$image = $this->createImageFromPath($this->liveFolder . $filename, $this->extension);
$originalSize = $this->getImageSize($image);
$thumbSize = $this->desiredSizes['thumbs'];
$thumb = $this->imageResize($image, $originalSize, ['thumbs' => $thumbSize ], $this->extension);
$saveImage = $this->saveImage($this->thumbFolder, $thumb['thumbs'], $this->filename, $this->extension);
if($saveImage)
{
return true;
}
return false;
}
# filename and imagepath can be a tmp-version after upload.
public function generateSizesFromImageFile($filename, $imagePath)
{
$this->setFileName($filename, 'image');
$image = $this->createImageFromPath($imagePath, $this->extension);
$originalSize = $this->getImageSize($image);
$resizedImages = $this->imageResize($image, $originalSize, $this->desiredSizes, $this->extension);
return $resizedImages;
}
public function grayscale($imagePath, $extension)
{
$image = $this->createImageFromPath($imagePath, $extension);
imagefilter($image, IMG_FILTER_GRAYSCALE);
return $image;
}
public function createImageFromPath($imagePath, $extension)
{
switch($extension)
{
case 'gif': $image = imagecreatefromgif($imagePath); break;
case 'jpg' :
case 'jpeg': $image = imagecreatefromjpeg($imagePath); break;
case 'png': $image = imagecreatefrompng($imagePath); break;
case 'webp': $image = imagecreatefromwebp($imagePath); break;
default: return 'image type not supported';
}
return $image;
}
}

View File

@@ -0,0 +1,229 @@
<?php
namespace Typemill\Static;
use Typemill\Models\StorageWrapper;
class License
{
public static function getLicenseData()
{
# returns data for settings page
}
public static function getLicensePlan()
{
# returns plan for plugins
}
# check the local licence file (like pem or pub)
public static function checkLicense()
{
$storage = new StorageWrapper('\Typemill\Models\Storage');
$licensedata = $storage->getYaml('settings', 'license.yaml');
if(!$licensedata)
{
return ['result' => false, 'message' => 'no license found'];
}
if(!isset($licensedata['license'],$licensedata['email'],$licensedata['domain'],$licensedata['plan'],$licensedata['payed_until'],$licensedata['signature']))
{
return ['result' => false, 'message' => 'License data not complete'];
}
$licenseStatus = self::validateLicense($licensedata);
unset($licensedata['signature']);
if($licenseStatus === false)
{
return ['result' => false, 'message' => 'License data are invalid'];
}
elseif($licenseStatus === true)
{
echo '<pre>';
print_r($licensedata);
die();
}
else
{
die('error checking signature');
}
}
public static function validateLicense($data)
{
$public_key_pem = self::getPublicKeyPem();
$binary_signature = base64_decode($data['signature']);
$data['email'] = self::hashMail($data['email']);
unset($data['signature']);
# manipulate data
# $data['product'] = 'business';
$data = json_encode($data);
# Check signature
$verified = openssl_verify($data, $binary_signature, $public_key_pem, OPENSSL_ALGO_SHA256);
if ($verified == 1)
{
return true;
}
elseif ($verified == 0)
{
return false;
}
else
{
die("ugly, error checking signature");
}
}
public static function hashMail($mail)
{
return hash('sha256', trim($mail) . 'TYla5xa8JUur');
}
public static function getPublicKeyPem()
{
$pkeyfile = getcwd() . DIRECTORY_SEPARATOR . 'settings' . DIRECTORY_SEPARATOR . "public_key.pem";
if(file_exists($pkeyfile) && is_readable($pkeyfile))
{
# fetch public key from file and ready it
$fp = fopen($pkeyfile, "r");
$public_key_pem = fread($fp, 8192);
fclose($fp);
return $public_key_pem;
}
return false;
}
}
/* KIRBY -> source -> cms -> system.php
/**
* Loads the license file and returns
* the license information if available
*
* @return string|bool License key or `false` if the current user has
* permissions for access.settings, otherwise just a
* boolean that tells whether a valid license is active
public function license()
{
try {
$license = Json::read($this->app->root('license'));
} catch (Throwable) {
return false;
}
// check for all required fields for the validation
if (isset(
$license['license'],
$license['order'],
$license['date'],
$license['email'],
$license['domain'],
$license['signature']
) !== true) {
return false;
}
// build the license verification data
$data = [
'license' => $license['license'],
'order' => $license['order'],
'email' => hash('sha256', $license['email'] . 'kwAHMLyLPBnHEskzH9pPbJsBxQhKXZnX'),
'domain' => $license['domain'],
'date' => $license['date']
];
// get the public key
$pubKey = F::read($this->app->root('kirby') . '/kirby.pub');
// verify the license signature
$data = json_encode($data);
$signature = hex2bin($license['signature']);
if (openssl_verify($data, $signature, $pubKey, 'RSA-SHA256') !== 1) {
return false;
}
// verify the URL
if ($this->licenseUrl() !== $this->licenseUrl($license['domain'])) {
return false;
}
// only return the actual license key if the
// current user has appropriate permissions
if ($this->app->user()?->isAdmin() === true) {
return $license['license'];
}
return true;
}
/**
* Validates the license key
* and adds it to the .license file in the config
* folder if possible.
*
* @throws \Kirby\Exception\Exception
* @throws \Kirby\Exception\InvalidArgumentException
*
public function register(string $license = null, string $email = null): bool
{
if (Str::startsWith($license, 'K3-PRO-') === false) {
throw new InvalidArgumentException(['key' => 'license.format']);
}
if (V::email($email) === false) {
throw new InvalidArgumentException(['key' => 'license.email']);
}
// @codeCoverageIgnoreStart
$response = Remote::get('https://hub.getkirby.com/register', [
'data' => [
'license' => $license,
'email' => Str::lower(trim($email)),
'domain' => $this->indexUrl()
]
]);
if ($response->code() !== 200) {
throw new Exception($response->content());
}
// decode the response
$json = Json::decode($response->content());
// replace the email with the plaintext version
$json['email'] = $email;
// where to store the license file
$file = $this->app->root('license');
// save the license information
Json::write($file, $json);
if ($this->license() === false) {
throw new InvalidArgumentException([
'key' => 'license.verification'
]);
}
// @codeCoverageIgnoreEnd
return true;
}
*/

View File

@@ -0,0 +1,45 @@
<?php
namespace Typemill\Static;
use \URLify;
class Slug
{
public static function getStringParts($name)
{
return preg_split('/[\-\.\_\=\+\?\!\*\#\(\)\/ ]/',$name);
}
public static function getFileType($fileName)
{
$parts = preg_split('/\./',$fileName);
return end($parts);
}
public static function splitFileName($fileName)
{
$parts = preg_split('/\./',$fileName);
return $parts;
}
public static function getNameWithoutType($fileName)
{
$parts = preg_split('/\./',$fileName);
return $parts[0];
}
public static function createSlug($name, $language = 'en')
{
$name = iconv(mb_detect_encoding($name, mb_detect_order(), true), "UTF-8", $name);
return URLify::filter(
$name,
$length = 60,
$language,
$file_name = false,
$use_remove_list = false,
$lower_case = true,
$treat_underscore_as_space = true
);
}
}

View File

@@ -0,0 +1,7 @@
pre code.hljs{display:block;overflow-x:auto;padding:1em}code.hljs{padding:3px 5px}/*!
Theme: a11y-dark
Author: @ericwbailey
Maintainer: @ericwbailey
Based on the Tomorrow Night Eighties theme: https://github.com/isagalaev/highlight.js/blob/master/src/styles/tomorrow-night-eighties.css
*/.hljs{background:#2b2b2b;color:#f8f8f2}.hljs-comment,.hljs-quote{color:#d4d0ab}.hljs-deletion,.hljs-name,.hljs-regexp,.hljs-selector-class,.hljs-selector-id,.hljs-tag,.hljs-template-variable,.hljs-variable{color:#ffa07a}.hljs-built_in,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-type{color:#f5ab35}.hljs-attribute{color:gold}.hljs-addition,.hljs-bullet,.hljs-string,.hljs-symbol{color:#abe338}.hljs-section,.hljs-title{color:#00e0e0}.hljs-keyword,.hljs-selector-tag{color:#dcc6e0}.hljs-emphasis{font-style:italic}.hljs-strong{font-weight:700}@media screen and (-ms-high-contrast:active){.hljs-addition,.hljs-attribute,.hljs-built_in,.hljs-bullet,.hljs-comment,.hljs-link,.hljs-literal,.hljs-meta,.hljs-number,.hljs-params,.hljs-quote,.hljs-string,.hljs-symbol,.hljs-type{color:highlight}.hljs-keyword,.hljs-selector-tag{font-weight:700}}

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,131 @@
const app = Vue.createApp({
template: `<Transition name="initial" appear>
<div v-if="licenseData.license">
<div>
<p v-if="!licenseData.datecheck" class="bg-rose-500 text-white p-2 text-center">Your license is out of date. Please check if the payments for your subscription were successfull.</p>
<p v-else-if="!licenseData.domaincheck" class="bg-rose-500 text-white p-2 text-center">Your license is only valid for the domain listed in your license data below.</p>
<p v-else>Congratulations! Your license is ok and you can enjoy all features.</p>
</div>
<div class="flex flex-wrap justify-between">
<div class="w-2/5 border-2 border-stone-200 my-8 text-center flex flex-col">
<div class="p-8 grow flex justify-center items-center">
<img class="mx-auto" :src="src" width="150" height="150">
</div>
<div class="p-8 bg-teal-500">
<p class="font-medium text-white">{{ licenseData.plan }}-LICENSE</p>
</div>
</div>
<div class="w-3/5 border-2 border-stone-200 p-8 my-8">
<p class="mb-1 font-medium">License-key:</p>
<p class="w-full border p-2 bg-stone-100">{{ licenseData.license }}</p>
<p class="mb-1 mt-3 font-medium">Domain:</p>
<p class="w-full border p-2 bg-stone-100">{{ licenseData.domain }}</p>
<p class="mb-1 mt-3 font-medium">E-Mail:</p>
<p class="w-full border p-2 bg-stone-100">{{ licenseData.email }}</p>
<p class="mb-1 mt-3 font-medium">Payed until:</p>
<p class="w-full border p-2 bg-stone-100">{{ licenseData.payed_until }}</p>
</div>
</div>
</div>
<form v-else class="inline-block w-full">
<div>
<p>Buy a typemill-license and enjoy our flatrate-model for premium-plugins and -themes.</p><p>We offer two types of subscription-based licenses:</p>
<div class="flex flex-wrap justify-between">
<div class="w-half border-2 border-stone-200 p-4 my-8 text-center">
<h2 class="text-3 font-bold mb-4">Maker License</h2>
<p class="mb-4">Use all maker-prodcuts (plugins and themes) for one year. The subscription will automatically renewed after a year.</p>
<a href="#!" class="paddle_button" data-product="44699">Buy now for 29,00 €/year</a>
</div>
<div class="w-half border-2 border-stone-200 p-4 my-8 text-center">
<h2 class="text-3 font-bold mb-4">Business License</h2>
<p class="mb-4">Use all business- and maker-products (plugins, themes, services) for one year. The subscription will automatically renewed after a year.</p>
<a href="#!" class="paddle_button" data-product="44700">Buy now for 229,00 €/year</a>
</div>
</div>
</div>
<div v-for="(fieldDefinition, fieldname) in formDefinitions">
<fieldset class="flex flex-wrap justify-between border-2 border-stone-200 p-4 my-8" v-if="fieldDefinition.type == 'fieldset'">
<legend class="text-lg font-medium">{{ fieldDefinition.legend }}</legend>
<component v-for="(subfieldDefinition, subfieldname) in fieldDefinition.fields"
:key="subfieldname"
:is="selectComponent(subfieldDefinition.type)"
:errors="errors"
:name="subfieldname"
:userroles="userroles"
:value="formData[subfieldname]"
v-bind="subfieldDefinition">
</component>
</fieldset>
<component v-else
:key="fieldname"
:is="selectComponent(fieldDefinition.type)"
:errors="errors"
:name="fieldname"
:userroles="userroles"
:value="formData[fieldname]"
v-bind="fieldDefinition">
</component>
</div>
<div class="my-5">
<div :class="messageClass" class="block w-full h-8 px-3 py-1 my-1 text-white transition duration-100">{{ message }}</div>
<input type="submit" @click.prevent="save()" value="save" class="w-full p-3 my-1 bg-stone-700 hover:bg-stone-900 text-white cursor-pointer transition duration-100">
</div>
</form>
</Transition>`,
data() {
return {
licenseData: data.licensedata,
formDefinitions: data.licensefields,
formData: {},
message: '',
messageClass: '',
errors: {},
src: tmaxios.defaults.baseURL + "/system/author/img/favicon-144.png"
}
},
mounted() {
eventBus.$on('forminput', formdata => {
this.formData[formdata.name] = formdata.value;
});
},
methods: {
selectComponent: function(type)
{
return 'component-'+type;
},
save: function()
{
this.reset();
var self = this;
tmaxios.post('/api/v1/license',{
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'license': this.formData
})
.then(function (response)
{
self.messageClass = 'bg-teal-500';
self.message = response.data.message;
self.licenseData = response.data.licensedata;
})
.catch(function (error)
{
self.messageClass = 'bg-rose-500';
self.message = error.response.data.message;
/* form validation errors */
if(error.response.data.errors !== undefined)
{
self.errors = error.response.data.errors;
}
});
},
reset: function()
{
this.errors = {};
this.message = '';
this.messageClass = '';
}
},
})

View File

@@ -0,0 +1,136 @@
const app = Vue.createApp({
template: `<Transition name="initial" appear>
<div class="w-full">
<form class="w-full my-8">
<div v-for="(fieldDefinition, fieldname) in formDefinitions">
<fieldset class="flex flex-wrap justify-between border-2 border-stone-200 p-4 my-8" v-if="fieldDefinition.type == 'fieldset'">
<legend class="text-lg font-medium">{{ fieldDefinition.legend }}</legend>
<component v-for="(subfieldDefinition, subfieldname) in fieldDefinition.fields"
:key="subfieldname"
:is="selectComponent(subfieldDefinition.type)"
:errors="errors"
:name="subfieldname"
:userroles="userroles"
:value="formData[subfieldname]"
v-bind="subfieldDefinition">
</component>
</fieldset>
<component v-else
:key="fieldname"
:is="selectComponent(fieldDefinition.type)"
:errors="errors"
:name="fieldname"
:userroles="userroles"
:value="formData[fieldname]"
v-bind="fieldDefinition">
</component>
</div>
<div class="my-5">
<div :class="messageClass" class="block w-full h-8 px-3 py-1 my-1 text-white transition duration-100">{{ message }}</div>
<button type="submit" @click.prevent="save()" class="w-full p-3 my-1 bg-stone-700 hover:bg-stone-900 text-white cursor-pointer transition duration-100">Save</button>
</div>
</form>
<div class="my-5 text-center">
<button @click.prevent="showModal = true" class="p-3 px-4 text-rose-500 border border-rose-100 hover:border-rose-500 cursor-pointer transition duration-100">delete user</button>
<modal v-if="showModal" @close="showModal = false">
<template #header>
<h3>Delete user</h3>
</template>
<template #body>
<p>Do you really want to delete this user?</p>
</template>
<template #button>
<button @click="deleteuser()" class="focus:outline-none px-4 p-3 mr-3 text-white bg-rose-500 hover:bg-rose-700 transition duration-100">delete user</button>
</template>
</modal>
</div>
</div>
</Transition>`,
data() {
return {
formDefinitions: data.userfields,
formData: data.userdata,
userroles: data.userroles,
message: '',
messageClass: '',
errors: {},
showModal: false,
}
},
mounted() {
eventBus.$on('forminput', formdata => {
this.formData[formdata.name] = formdata.value;
});
},
methods: {
selectComponent: function(type)
{
return 'component-'+type;
},
changeForm: function()
{
/* change input form if user role changed */
},
save: function()
{
this.reset();
var self = this;
tmaxios.put('/api/v1/user',{
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'userdata': this.formData
})
.then(function (response)
{
self.messageClass = 'bg-teal-500';
self.message = response.data.message;
})
.catch(function (error)
{
self.messageClass = 'bg-rose-500';
self.message = error.response.data.message;
if(error.response.data.errors !== undefined)
{
self.errors = error.response.data.errors;
}
});
},
deleteuser: function()
{
this.reset();
var self = this;
tmaxios.delete('/api/v1/user',{
data: {
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'username': this.formData.username
}
})
.then(function (response)
{
self.showModal = false;
self.messageClass = 'bg-teal-500';
self.message = response.data.message;
/* redirect to userlist */
})
.catch(function (error)
{
self.showModal = false;
self.messageClass = 'bg-rose-500';
self.message = error.response.data.message;
if(error.response.data.errors !== undefined)
{
self.errors = error.response.data.errors;
}
});
},
reset: function()
{
this.errors = {};
this.message = '';
this.messageClass = '';
}
},
})

View File

@@ -0,0 +1,126 @@
const app = Vue.createApp({
template: `<Transition name="initial" appear>
<div class="w-full">
<div class="mt-5 mb-5">
<label for="roleselector" class="block mb-1 font-medium">{{ $filters.translate("Select a role") }}</label>
<select class="form-select block w-full border border-stone-300 bg-stone-200 px-2 py-3 h-12 transition ease-in-out"
v-model="selectedrole"
@change="generateForm()">
<option disabled value="">Please select</option>
<option v-for="option,optionkey in userroles">{{option}}</option>
</select>
</div>
<form v-if="formDefinitions" class="w-full my-8">
<div v-for="(fieldDefinition, fieldname) in formDefinitions">
<fieldset class="flex flex-wrap justify-between border-2 border-stone-200 p-4 my-8" v-if="fieldDefinition.type == 'fieldset'">
<legend class="text-lg font-medium">{{ fieldDefinition.legend }}</legend>
<component v-for="(subfieldDefinition, subfieldname) in fieldDefinition.fields"
:key="subfieldname"
:is="selectComponent(subfieldDefinition.type)"
:errors="errors"
:name="subfieldname"
:userroles="userroles"
:value="formData[subfieldname]"
v-bind="subfieldDefinition">
</component>
</fieldset>
<component v-else
:key="fieldname"
:is="selectComponent(fieldDefinition.type)"
:errors="errors"
:name="fieldname"
:userroles="userroles"
:value="formData[fieldname]"
v-bind="fieldDefinition">
</component>
</div>
<div class="my-5">
<div :class="messageClass" class="block w-full h-8 px-3 py-1 my-1 text-white transition duration-100">{{ message }}</div>
<button type="submit" @click.prevent="save()" class="w-full p-3 my-1 bg-stone-700 hover:bg-stone-900 text-white cursor-pointer transition duration-100">Save</button>
</div>
</form>
</div>
</Transition>`,
data() {
return {
selectedrole: false,
formDefinitions: false,
formData: {},
userroles: data.userroles,
message: '',
messageClass: '',
errors: {},
}
},
mounted() {
eventBus.$on('forminput', formdata => {
this.formData[formdata.name] = formdata.value;
});
},
methods: {
selectComponent: function(type)
{
return 'component-'+type;
},
generateForm: function()
{
this.reset();
var self = this;
tmaxios.get('/api/v1/userform',{
params: {
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'userrole': this.selectedrole
}
})
.then(function (response)
{
self.formDefinitions = response.data.userform;
self.formData.userrole = self.selectedrole;
})
.catch(function (error)
{
self.messageClass = 'bg-rose-500';
self.message = error.response.data.message;
if(error.response.data.errors !== undefined)
{
self.errors = error.response.data.errors;
}
});
},
save: function()
{
this.reset();
var self = this;
tmaxios.post('/api/v1/user',{
'csrf_name': document.getElementById("csrf_name").value,
'csrf_value': document.getElementById("csrf_value").value,
'userdata': this.formData
})
.then(function (response)
{
self.messageClass = 'bg-teal-500';
self.message = response.data.message;
window.location = tmaxios.defaults.baseURL + '/tm/user/' + self.formData.username;
})
.catch(function (error)
{
self.messageClass = 'bg-rose-500';
self.message = error.response.data.message;
if(error.response.data.errors !== undefined)
{
self.errors = error.response.data.errors;
}
});
},
reset: function()
{
this.errors = {};
this.message = '';
this.messageClass = '';
}
},
})

View File

@@ -0,0 +1,27 @@
{% extends 'layouts/layoutSystem.twig' %}
{% block title %}{{ translate('License') }}{% endblock %}
{% block content %}
<h1 class="text-3xl font-bold mb-4">{{ translate('License') }} </h1>
<div id="license" v-cloak></div>
{% endblock %}
{% block javascript %}
<script src="{{ base_url() }}/system/typemill/author/js/vue-license.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-translate.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-shared.js?v={{ settings.version }}"></script>
<script>
app.mount('#license');
</script>
<script src="https://cdn.paddle.com/paddle/paddle.js"></script>
<script type="text/javascript">
Paddle.Environment.set('sandbox');
Paddle.Setup({ vendor: 10629 });
</script>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends 'layouts/layoutSystem.twig' %}
{% block title %}{{ translate('Account') }}{% endblock %}
{% block content %}
<h1 class="text-3xl font-bold mb-4">{{ translate('User') }} </h1>
<div id="account" v-cloak></div>
{% endblock %}
{% block javascript %}
<script src="{{ base_url() }}/system/typemill/author/js/vue-user.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-translate.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-shared.js?v={{ settings.version }}"></script>
<script>
app.mount('#account');
</script>
{% endblock %}

View File

@@ -0,0 +1,21 @@
{% extends 'layouts/layoutSystem.twig' %}
{% block title %}{{ translate('Create user') }}{% endblock %}
{% block content %}
<h1 class="text-3xl font-bold mb-4">{{ translate('Create user') }} </h1>
<div id="newuser" v-cloak></div>
{% endblock %}
{% block javascript %}
<script src="{{ base_url() }}/system/typemill/author/js/vue-usernew.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-translate.js?v={{ settings.version }}"></script>
<script src="{{ base_url() }}/system/typemill/author/js/vue-shared.js?v={{ settings.version }}"></script>
<script>
app.mount('#newuser');
</script>
{% endblock %}

View File

@@ -0,0 +1,15 @@
license:
name: license
label: 'Your license key'
type: 'text'
required: true
email:
name: email
label: 'Your email'
type: 'text'
required: true
domain:
name: domain
label: 'Domain for license'
type: 'text'
required: true