Merge pull request #25 from bobdenotter/feature/uploader

First version of Filepond uploader
This commit is contained in:
Bob den Otter
2018-10-18 14:51:52 +02:00
committed by GitHub
22 changed files with 936 additions and 58 deletions

View File

@@ -43,4 +43,6 @@ $(document).ready(function() {
// $(".ui.calendar").calendar({
// ampm: false
// });
var lightbox = $('a.lightbox').simpleLightbox();
});

View File

@@ -25,3 +25,13 @@
font-size: 0.9rem;
}
}
.sl-overlay {
background: #000;
opacity: 0.85;
}
.sl-close {
color: #FFF;
}

View File

@@ -261,41 +261,40 @@ blocks:
slug:
type: slug
uses: [ title ]
selectfield:
type: select
values: [ foo, bar, baz ]
selectfieldd:
type: select
values:
foo: "Fooo"
bar: Bario
baz: Bazz
multiselect:
type: select
values: [ A-tuin, Donatello, Rafael, Leonardo, Michelangelo, Koopa, Squirtle ]
multiple: false
postfix: "Select your favourite turtle(s)."
multiselect2:
type: select
values: [ A-tuin, Donatello, Rafael, Leonardo, Michelangelo, Koopa, Squirtle ]
multiple: true
required: true
postfix: "Select your favourite turtle(s)."
# content:
# type: html
# height: 150px
# contentlink:
# type: text
# label: Link
# placeholder: 'contenttype/slug or http://example.org/'
# postfix: "Use this to add a link for this Block. This could either be an 'internal' link like <tt>page/about</tt>, if you use a contenttype/slug combination. Otherwise use a proper URL, like `http://example.org`."
# image:
# type: image
# attrib: title
# extensions: [ gif, jpg, png ]
# selectfield:
# type: select
# values: [ foo, bar, baz ]
# selectfieldd:
# type: select
# values:
# foo: "Fooo"
# bar: Bario
# baz: Bazz
# multiselect:
# type: select
# values: [ A-tuin, Donatello, Rafael, Leonardo, Michelangelo, Koopa, Squirtle ]
# multiple: false
# postfix: "Select your favourite turtle(s)."
#
# multiselect2:
# type: select
# values: [ A-tuin, Donatello, Rafael, Leonardo, Michelangelo, Koopa, Squirtle ]
# multiple: true
# required: true
# postfix: "Select your favourite turtle(s)."
content:
type: html
height: 150px
contentlink:
type: text
label: Link
placeholder: 'contenttype/slug or http://example.org/'
postfix: "Use this to add a link for this Block. This could either be an 'internal' link like <tt>page/about</tt>, if you use a contenttype/slug combination. Otherwise use a proper URL, like `http://example.org`."
image:
type: image
attrib: title
extensions: [ gif, jpg, png ]
show_on_dashboard: true
viewless: true
default_status: published

View File

@@ -1 +1 @@
nav.flex-column{background-color:#345}nav.flex-column hr{border-top-width:0;border-bottom:1px solid hsla(0,0%,100%,.2);margin:0}nav.flex-column .logo{color:#fff;background:#345;text-align:center;margin:1rem}nav.flex-column .logo h2{font-size:36px}nav.flex-column .nav-item{color:#ddd!important;padding-top:.6rem;padding-bottom:.6rem;text-align:left}nav.flex-column .nav-item a{color:#ddd!important}nav.flex-column .nav-item .fa-stack{height:2.3em;margin-right:.5rem}nav.flex-column .nav-item .fa-stack i:last-child{color:#444}nav.flex-column .nav-item>i.dropdown.icon{margin-top:10px}nav.flex-column .nav-item.separator{padding:1rem 1rem .5rem;color:hsla(0,0%,78%,.5)!important}nav.flex-column .nav-item.separator .fas{padding:0 1.1rem 0 .65rem}nav.flex-column .nav-item.active,nav.flex-column .nav-item.current{background-color:#41647f!important;color:#fff!important}nav.flex-column .nav-item.active>a,nav.flex-column .nav-item.current>a{color:#fff!important}nav.flex-column .dropdown-menu{transform:translateX(140px)!important;padding-bottom:0}nav.flex-column .dropdown-menu a{padding:.25rem .75rem}nav.flex-column .dropdown-menu .btn-group{width:100%;background-color:#eee;border-top:1px solid #ddd;margin-top:.5rem;display:flex}nav.flex-column .dropdown-menu .btn{background-color:#eee;border:0;flex:1;padding:.5rem 0}nav.flex-column .dropdown-menu .btn:hover{background-color:#ccc;border:0}.nav.btn-toolbar{margin:.5rem 1rem 0 3rem}.nav.btn-toolbar .nav-item{flex-grow:0;margin-right:.6rem}.nav.btn-toolbar .nav-item.topbar-title{font-family:Source Sans Pro,serif;font-size:22px;color:#222;overflow:hidden;text-overflow:ellipsis;text-align:left;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;flex-grow:1}.nav.btn-toolbar .btn-light{background-color:#eee;border:1px solid #d8d8d8}.status-published{color:#7fa800}.status-held{color:#ca2300}.status-timed{color:#a80}.status-draft{color:#0569e2}.card ul{padding-left:1.2rem}body,html{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:13px;line-height:1.5;height:100%;margin:0;padding:0}@media only screen and (min-width:1025px){body,html{font-size:14px}}@media only screen and (min-width:1281px){body,html{font-size:15px}}body{background:#f7f7f7}h1,h2,h3,h4,h5,h6{font-family:Source Sans Pro,serif;font-weight:400}h1{font-weight:700;font-size:2.5rem}h2{font-size:2rem;line-height:4rem}h3{font-size:1.75rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1rem}.wrapper{display:grid;width:100vw;height:100vh;grid-template-areas:"topbar" "sidebar" "content" "sidebar2" "footer"}@media only screen and (min-width:600px){.wrapper{grid-template-areas:"sidebar topbar topbar" "sidebar content aside" "sidebar footer footer"}.wrapper,.wrapper.has-widecontent{grid-template-columns:12.6rem auto 21rem;grid-template-rows:3.6rem auto 2rem}.wrapper.has-widecontent{grid-template-areas:"sidebar topbar topbar" "sidebar content content" "sidebar footer footer"}}#sidebar{grid-area:sidebar;background-color:#345;border-right:1px solid #233}header{grid-area:topbar;background-color:#fff;border-bottom:1px solid #ddd}#content,#vuecontent,#widecontent{grid-area:content;padding:2rem 3rem}#widecontent+aside{display:none}footer{grid-area:footer}aside{grid-area:aside;padding:2rem 2rem 2rem 0}.ui.basic.table{background-color:#fff!important}.ui.basic.table th{background-color:#f8f8f8}.ui.basic.table td{padding:.7em .4em}#editcontent input.large{font-size:140%;height:auto}#editcontent input[readonly]{background-color:#f2f2f2;color:#888}#editcontent .input-group-append .btn{font-weight:400;font-size:.9rem}.imageholder{width:100%;border-radius:5px;border:1px solid #ddd;background-color:#fff}.imageholder img{max-width:100%;display:block;margin:auto;border-radius:4px}.selectize-input>*{font-size:1rem;line-height:1.5rem}.clip-overflow{display:block;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis;max-height:4.5rem}td.listing-excerpt{color:#333}td.listing-meta{font-size:.9rem;line-height:1.5rem}td.listing-meta span{color:#666;display:inline-block;white-space:nowrap}td.listing-thumb img{display:block;width:6.25rem;height:4.5rem}td.listing-actions{white-space:nowrap}td.listing-actions .btn-light{background-color:#e0e0e0;border:1px solid #d8d8d8;color:#444}td.listing-actions .show>.btn-light.dropdown-toggle{background-color:#ccc;border:1px solid #b8b8b8;color:#444}.selectize-dropdown-content,.selectize-input{font-size:1rem}
nav.flex-column{background-color:#345}nav.flex-column hr{border-top-width:0;border-bottom:1px solid hsla(0,0%,100%,.2);margin:0}nav.flex-column .logo{color:#fff;background:#345;text-align:center;margin:1rem}nav.flex-column .logo h2{font-size:36px}nav.flex-column .nav-item{color:#ddd!important;padding-top:.6rem;padding-bottom:.6rem;text-align:left}nav.flex-column .nav-item a{color:#ddd!important}nav.flex-column .nav-item .fa-stack{height:2.3em;margin-right:.5rem}nav.flex-column .nav-item .fa-stack i:last-child{color:#444}nav.flex-column .nav-item>i.dropdown.icon{margin-top:10px}nav.flex-column .nav-item.separator{padding:1rem 1rem .5rem;color:hsla(0,0%,78%,.5)!important}nav.flex-column .nav-item.separator .fas{padding:0 1.1rem 0 .65rem}nav.flex-column .nav-item.active,nav.flex-column .nav-item.current{background-color:#41647f!important;color:#fff!important}nav.flex-column .nav-item.active>a,nav.flex-column .nav-item.current>a{color:#fff!important}nav.flex-column .dropdown-menu{transform:translateX(140px)!important;padding-bottom:0}nav.flex-column .dropdown-menu a{padding:.25rem .75rem}nav.flex-column .dropdown-menu .btn-group{width:100%;background-color:#eee;border-top:1px solid #ddd;margin-top:.5rem;display:flex}nav.flex-column .dropdown-menu .btn{background-color:#eee;border:0;flex:1;padding:.5rem 0}nav.flex-column .dropdown-menu .btn:hover{background-color:#ccc;border:0}.nav.btn-toolbar{margin:.5rem 1rem 0 3rem}.nav.btn-toolbar .nav-item{flex-grow:0;margin-right:.6rem}.nav.btn-toolbar .nav-item.topbar-title{font-family:Source Sans Pro,serif;font-size:22px;color:#222;overflow:hidden;text-overflow:ellipsis;text-align:left;display:-webkit-box;-webkit-line-clamp:1;-webkit-box-orient:vertical;flex-grow:1}.nav.btn-toolbar .btn-light{background-color:#eee;border:1px solid #d8d8d8}.status-published{color:#7fa800}.status-held{color:#ca2300}.status-timed{color:#a80}.status-draft{color:#0569e2}.card ul{padding-left:1.2rem}body,html{font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;font-size:13px;line-height:1.5;height:100%;margin:0;padding:0}@media only screen and (min-width:1025px){body,html{font-size:14px}}@media only screen and (min-width:1281px){body,html{font-size:15px}}body{background:#f7f7f7}h1,h2,h3,h4,h5,h6{font-family:Source Sans Pro,serif;font-weight:400}h1{font-weight:700;font-size:2.5rem}h2{font-size:2rem;line-height:4rem}h3{font-size:1.75rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1rem}.wrapper{display:grid;width:100vw;height:100vh;grid-template-areas:"topbar" "sidebar" "content" "sidebar2" "footer"}@media only screen and (min-width:600px){.wrapper{grid-template-areas:"sidebar topbar topbar" "sidebar content aside" "sidebar footer footer"}.wrapper,.wrapper.has-widecontent{grid-template-columns:12.6rem auto 21rem;grid-template-rows:3.6rem auto 2rem}.wrapper.has-widecontent{grid-template-areas:"sidebar topbar topbar" "sidebar content content" "sidebar footer footer"}}#sidebar{grid-area:sidebar;background-color:#345;border-right:1px solid #233}header{grid-area:topbar;background-color:#fff;border-bottom:1px solid #ddd}#content,#vuecontent,#widecontent{grid-area:content;padding:2rem 3rem}#widecontent+aside{display:none}footer{grid-area:footer}aside{grid-area:aside;padding:2rem 2rem 2rem 0}.ui.basic.table{background-color:#fff!important}.ui.basic.table th{background-color:#f8f8f8}.ui.basic.table td{padding:.7em .4em}#editcontent input.large{font-size:140%;height:auto}#editcontent input[readonly]{background-color:#f2f2f2;color:#888}#editcontent .input-group-append .btn{font-weight:400;font-size:.9rem}.sl-overlay{background:#000;opacity:.85}.sl-close{color:#fff}.imageholder{width:100%;border-radius:5px;border:1px solid #ddd;background-color:#fff}.imageholder img{max-width:100%;display:block;margin:auto;border-radius:4px}.selectize-input>*{font-size:1rem;line-height:1.5rem}.clip-overflow{display:block;display:-webkit-box;-webkit-line-clamp:3;-webkit-box-orient:vertical;overflow:hidden;text-overflow:ellipsis;max-height:4.5rem}td.listing-excerpt{color:#333}td.listing-meta{font-size:.9rem;line-height:1.5rem}td.listing-meta span{color:#666;display:inline-block;white-space:nowrap}td.listing-thumb img{display:block;width:6.25rem;height:4.5rem}td.listing-actions{white-space:nowrap}td.listing-actions .btn-light{background-color:#e0e0e0;border:1px solid #d8d8d8;color:#444}td.listing-actions .show>.btn-light.dropdown-toggle{background-color:#ccc;border:1px solid #b8b8b8;color:#444}.selectize-dropdown-content,.selectize-input{font-size:1rem}

File diff suppressed because one or more lines are too long

View File

@@ -94,10 +94,11 @@ class Config
/**
* @param string $path
* @param bool $absolute
* @param mixed $additional
*
* @return string
*/
public function getPath(string $path, bool $absolute = true, string $additional = ''): string
public function getPath(string $path, bool $absolute = true, $additional = null): string
{
return $this->pathResolver->resolve($path, $absolute, $additional);
}

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Bolt\Configuration;
use Exception;
use Tightenco\Collect\Support\Collection;
use Webmozart\PathUtil\Path;
@@ -92,12 +93,13 @@ class PathResolver
* - `foo/bar` - A relative path that will be resolved against the root path.
* - `/tmp` - An absolute path will be returned as is.
*
* @param string $path the path
* @param bool $absolute if the path is relative, resolve it against the root path
* @param string $path the path
* @param bool $absolute if the path is relative, resolve it against the root path
* @param mixed $additional
*
* @return string
*/
public function resolve(string $path, bool $absolute = true, string $additional = ''): string
public function resolve(string $path, bool $absolute = true, $additional = null): string
{
if (isset($this->paths[$path])) {
$path = $this->paths[$path];
@@ -129,8 +131,8 @@ class PathResolver
$path = Path::makeAbsolute($path, $this->paths['root']);
}
if ($additional !== '') {
$path .= '/' . $additional;
if (!empty($additional)) {
$path .= \DIRECTORY_SEPARATOR . implode(\DIRECTORY_SEPARATOR, (array) $additional);
}
// Make sure we don't have lingering unneeded dir-seperators

View File

@@ -7,14 +7,19 @@ declare(strict_types=1);
namespace Bolt\Content;
use Bolt\Common\Json;
use Bolt\Configuration\Config;
use Bolt\Entity\Media;
use Bolt\Media\Item;
use Bolt\Repository\MediaRepository;
use Carbon\Carbon;
use Cocur\Slugify\Slugify;
use Faker\Factory;
use PHPExif\Reader\Reader;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\SplFileInfo;
use Webmozart\PathUtil\Path;
class MediaFactory
{
@@ -116,4 +121,56 @@ class MediaFactory
return $user;
}
/**
* @param Item $item
* @param $params
*
* @return Media
*/
public function createFromUpload(Item $item, $params): Media
{
if (Json::test($params)) {
$params = Json::parse($params);
$addedPath = $params['path'];
$area = $params['area'];
} else {
$addedPath = '';
$area = 'files';
}
$targetFilename = $addedPath . \DIRECTORY_SEPARATOR . $this->sanitiseFilename($item->getName());
$source = $this->config->getPath('cache', true, ['uploads', $item->getId(), $item->getName()]);
$target = $this->config->getPath($area, true, $targetFilename);
$relPath = Path::getDirectory($targetFilename);
$relName = Path::getFilename($targetFilename);
// Move the file over
$fileSystem = new Filesystem();
$fileSystem->rename($source, $target, true);
$file = new SplFileInfo($target, $relPath, $relName);
$media = $this->createOrUpdateMedia($file, $area);
return $media;
}
/**
* @param string $filename
*
* @return string
*/
private function sanitiseFilename(string $filename): string
{
$extensionSlug = new Slugify(['regexp' => '/([^a-z0-9]|-)+/']);
$filenameSlug = new Slugify(['lowercase' => false]);
$extension = $extensionSlug->slugify(Path::getExtension($filename));
$filename = $filenameSlug->slugify(Path::getFilenameWithoutExtension($filename));
return $filename . '.' . $extension;
}
}

View File

@@ -0,0 +1,58 @@
<?php
declare(strict_types=1);
namespace Bolt\Controller\Async;
use Bolt\Content\MediaFactory;
use Bolt\Media\RequestHandler;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Annotation\Route;
class Uploader
{
/** @var MediaFactory */
private $mediaFactory;
/** @var RequestHandler */
private $requestHandler;
/** @var ObjectManager */
private $manager;
public function __construct(MediaFactory $mediaFactory, RequestHandler $requestHandler, ObjectManager $manager)
{
$this->mediaFactory = $mediaFactory;
$this->requestHandler = $requestHandler;
$this->manager = $manager;
}
/**
* @Route("/async/upload", name="bolt_upload_post", methods={"POST"})
*/
public function upload(Request $request)
{
// Get submitted field data item, will always be one item in case of async upload
$items = $this->requestHandler->loadFilesByField('filepond');
// If no items, exit
if (count($items) === 0) {
// Something went wrong, most likely a field name mismatch
http_response_code(400);
return;
}
$params = $request->request->get('filepond', []);
foreach ($items as $item) {
$media = $this->mediaFactory->createFromUpload($item, current($params));
$this->manager->persist($media);
$this->manager->flush();
}
return new Response($media->getPath() . $media->getFilename());
}
}

View File

@@ -41,6 +41,10 @@ class EditRecordController extends BaseController
* @param Request $request
* @param Content|null $content
*
* @throws \Twig_Error_Loader
* @throws \Twig_Error_Runtime
* @throws \Twig_Error_Syntax
*
* @return Response
*/
public function edit(string $id, Request $request, Content $content = null): Response

71
src/Media/Item.php Normal file
View File

@@ -0,0 +1,71 @@
<?php
declare(strict_types=1);
namespace Bolt\Media;
/**
* A wrapper class for easier access to $_FILES object.
*/
class Item
{
// counter that helps in ensuring each file receives a truly unique id
public static $item_counter = 0;
// item props
private $id;
private $file;
private $name;
public function __construct($file, $id = null)
{
$this->id = isset($id) ? $id : md5(uniqid(self::$item_counter++, true));
$this->file = $file;
$this->name = $file['name'];
}
public function rename($name, $extension = null)
{
$info = pathinfo($this->name);
$this->name = $name . '.' . (isset($extension) ? $extension : $info['extension']);
}
public function getId()
{
return $this->id;
}
public function getFilename()
{
return $this->file['tmp_name'];
}
public function getName()
{
return basename($this->name);
}
public function getNameWithoutExtension()
{
$info = pathinfo($this->name);
return $info['filename'];
}
public function getExtension()
{
$info = pathinfo($this->name);
return $info['extension'];
}
public function getSize()
{
return $this->file['size'];
}
public function getType()
{
return $this->file['mime'];
}
}

View File

@@ -0,0 +1,469 @@
<?php
declare(strict_types=1);
namespace Bolt\Media;
use Bolt\Configuration\Config;
/**
* FilePond RequestHandler helper class.
*/
/*
1. get files (from $files and $post)
2. store files in tmp/ directory and give them a unique server id
3. return server id's to client
4. either client reverts upload or finalizes form
5. call revert($server_id) to remove file from tmp/ directory
6. call save() to save file to final directory
*/
class RequestHandler
{
// the default location to save tmp files to
private $tmp_dir;
// regex to use for testing if a string is a file id
private $file_id_format = '/^[0-9a-fA-F]{32}$/';
/** @var Config */
private $config;
public function __construct(Config $config)
{
$this->config = $config;
$this->tmp_dir = $this->config->getPath('cache', true, ['uploads']);
}
/**
* @param $str
*
* @return bool
*/
public function isFileId($str)
{
return preg_match($this->file_id_format, $str);
}
/**
* @param $str
*
* @return bool
*/
public function isURL($str)
{
return filter_var($str, FILTER_VALIDATE_URL);
}
/**
* Catch all exceptions so we can return a 500 error when the server bugs out.
*/
public function catchExceptions()
{
set_exception_handler('FilePond\RequestHandler::handleException');
}
public function handleException($ex)
{
// write to error log so we can still find out what's up
error_log('Uncaught exception in class="' . get_class($ex) . '" message="' . $ex->getMessage() . '" line="' . $ex->getLine() . '"');
// clean up buffer
ob_end_clean();
// server error mode go!
http_response_code(500);
}
private function createItem($file, $id = null)
{
return new Item($file, $id);
}
/**
* @param $fieldName
*
* @return array
*/
public function loadFilesByField($fieldName)
{
// See if files are posted as JSON string (each file being base64 encoded)
$base64Items = $this->loadBase64FormattedFiles($fieldName);
// retrieves posted file objects
$fileItems = $this->loadFileObjects($fieldName);
// retrieves files already on server
$tmpItems = $this->loadFilesFromTemp($fieldName);
// save newly received files to temp files folder (tmp items already are in that folder)
$this->saveAsTempFiles(array_merge($base64Items, $fileItems));
// return items
return array_merge($base64Items, $fileItems, $tmpItems);
}
private function loadFileObjects($fieldName)
{
$items = [];
if (!isset($_FILES[$fieldName])) {
return $items;
}
$FILE = $_FILES[$fieldName];
if (is_array($FILE['tmp_name'])) {
foreach ($FILE['tmp_name'] as $index => $tmpName) {
array_push($items, $this->createItem([
'tmp_name' => $FILE['tmp_name'][$index],
'name' => $FILE['name'][$index],
'size' => $FILE['size'][$index],
'error' => $FILE['error'][$index],
'type' => $FILE['type'][$index],
]));
}
} else {
array_push($items, $this->createItem($FILE));
}
return $items;
}
private function loadBase64FormattedFiles($fieldName)
{
/*
// format:
{
"id": "iuhv2cpsu",
"name": "picture.jpg",
"type": "image/jpeg",
"size": 20636,
"metadata" : {...}
"data": "/9j/4AAQSkZJRgABAQEASABIAA..."
}
*/
$items = [];
if (!isset($_POST[$fieldName])) {
return $items;
}
// Handle posted files array
$values = $_POST[$fieldName];
// Turn values in array if is submitted as single value
if (!is_array($values)) {
$values = isset($values) ? [$values] : [];
}
// If files are found, turn base64 strings into actual file objects
foreach ($values as $value) {
// suppress error messages, we'll just investigate the object later
$obj = @json_decode($value);
// skip values that failed to be decoded
if (!isset($obj)) {
continue;
}
// test if this is a file object (matches the object described above)
if (!$this->isEncodedFile($obj)) {
continue;
}
array_push($items, $this->createItem($this->createTempFile($obj)));
}
return $items;
}
private function isEncodedFile($obj)
{
return isset($obj->id) && isset($obj->data) && isset($obj->name) && isset($obj->type) && isset($obj->size);
}
private function loadFilesFromTemp($fieldName)
{
$items = [];
if (!isset($_POST[$fieldName])) {
return $items;
}
// Handle posted ids array
$values = $_POST[$fieldName];
// Turn values in array if is submitted as single value
if (!is_array($values)) {
$values = isset($values) ? [$values] : [];
}
// test if value is actually a file id
foreach ($values as $value) {
if ($this->isFileId($value)) {
array_push($items, $this->createItem($this->getTempFile($value), $value));
}
}
return $items;
}
public function save($items, $path = 'uploads' . \DIRECTORY_SEPARATOR)
{
// is list of files
if (is_array($items)) {
$results = [];
foreach ($items as $item) {
array_push($results, $this->saveFile($item, $path));
}
return $results;
}
// is single item
return $this->saveFile($items, $path);
}
/**
* @param $file_id
*
* @return bool
*/
public function deleteTempFile($file_id)
{
return $this->deleteTempDirectory($file_id);
}
/**
* @param $url
*
* @return array|bool
*/
public function getRemoteURLData($url)
{
try {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
$content = curl_exec($ch);
if ($content === false) {
throw new Exception(curl_error($ch), curl_errno($ch));
}
$type = curl_getinfo($ch, CURLINFO_CONTENT_TYPE);
$length = curl_getinfo($ch, CURLINFO_CONTENT_LENGTH_DOWNLOAD);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
$success = $code >= 200 && $code < 300;
return [
'code' => $code,
'content' => $content,
'type' => $type,
'length' => $length,
'success' => $success,
];
} catch (Exception $e) {
return null;
}
}
private function saveAsTempFiles($items)
{
foreach ($items as $item) {
$this->saveTempFile($item);
}
}
private function saveTempFile($file)
{
// make sure path name is safe
$path = $this->getSecureTempPath() . $file->getId() . \DIRECTORY_SEPARATOR;
// Creates a secure temporary directory to store the files in
$this->createSecureDirectory($path);
// get source and target values
$source = $file->getFilename();
$target = $path . $file->getName();
// Move uploaded file to this new secure directory
$result = $this->moveFile($source, $target);
// Was not saved
if ($result !== true) {
return $result;
}
// Make sure file is secure
$this->setSecureFilePermissions($target);
// temp file stored successfully
return true;
}
public function getTempFile($fileId)
{
// select all files in directory except .htaccess
foreach (glob($this->getSecureTempPath() . $fileId . \DIRECTORY_SEPARATOR . '*.*') as $file) {
try {
$handle = fopen($file, 'rb');
$content = fread($handle, filesize($file));
fclose($handle);
return [
'name' => basename($file),
'content' => $content,
'type' => mime_content_type($file),
'length' => filesize($file),
];
} catch (Exception $e) {
return null;
}
}
return false;
}
public function getFile($file, $path)
{
try {
$filename = $path . \DIRECTORY_SEPARATOR . $file;
$handle = fopen($filename, 'rb');
$content = fread($handle, filesize($filename));
fclose($handle);
return [
'name' => basename($filename),
'content' => $content,
'type' => mime_content_type($filename),
'length' => filesize($filename),
];
} catch (Exception $e) {
return null;
}
}
private function saveFile($item, $path)
{
// nope
if (!isset($item)) {
return false;
}
// if is file id
if (is_string($item)) {
return $this->moveFileById($item, $path);
}
// is file object
return $this->moveFileById($item->getId(), $path, $item->getName());
}
private function moveFileById($fileId, $path, $fileName = null)
{
// select all files in directory except .htaccess
foreach (glob($this->getSecureTempPath() . $fileId . \DIRECTORY_SEPARATOR . '*.*') as $file) {
$source = $file;
$target = $this->getSecurePath($path);
$this->createDirectory($target);
rename($source, $target . (isset($fileName) ? basename($fileName) : basename($file)));
}
// remove directory
$this->deleteTempDirectory($fileId);
// done!
return true;
}
private function deleteTempDirectory($id)
{
@array_map('unlink', glob($this->getSecureTempPath() . $id . \DIRECTORY_SEPARATOR . '{.,}*', GLOB_BRACE));
// remove temp directory
@rmdir($this->getSecureTempPath() . $id);
}
private function createTempFile($file)
{
$tmp = tmpfile();
fwrite($tmp, base64_decode($file->data, true));
$meta = stream_get_meta_data($tmp);
$filename = $meta['uri'];
return [
'error' => 0,
'size' => filesize($filename),
'type' => $file->type,
'name' => $file->name,
'tmp_name' => $filename,
'tmp' => $tmp,
];
}
private function moveFile($source, $target)
{
if (is_uploaded_file($source)) {
return move_uploaded_file($source, $target);
}
$tmp = fopen($source, 'rb');
$result = file_put_contents($target, fread($tmp, filesize($source)));
fclose($tmp);
return $result;
}
private function getSecurePath($path)
{
return pathinfo($path)['dirname'] . \DIRECTORY_SEPARATOR . basename($path) . \DIRECTORY_SEPARATOR;
}
private function getSecureTempPath()
{
return $this->getSecurePath($this->tmp_dir);
}
private function setSecureFilePermissions($target)
{
$stat = stat(dirname($target));
$perms = $stat['mode'] & 0000666;
@chmod($target, $perms);
}
private function createDirectory($path)
{
if (is_dir($path)) {
return false;
}
mkdir($path, 0755, true);
return true;
}
private function createSecureDirectory($path)
{
// !! If directory already exists we assume security is handled !!
// Test if directory already exists and correct
if ($this->createDirectory($path)) {
// Add .htaccess file for security purposes
$content = '# Don\'t list directory contents
IndexIgnore *
# Disable script execution
AddHandler cgi-script .php .pl .jsp .asp .sh .cgi
Options -ExecCGI -Indexes';
file_put_contents($path . '.htaccess', $content);
}
}
}

View File

@@ -10,6 +10,7 @@
<link href="https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400,700" rel="stylesheet">
<link rel="stylesheet" href="https://use.fontawesome.com/releases/v5.3.1/css/all.css" integrity="sha384-mzrmE5qonljUremFsqc01SB46JvROS7bZs3IO2EmfFsd15uHvIt+Y8vEf7N7fWAU" crossorigin="anonymous">
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/css/bootstrap.min.css" integrity="sha384-MCw98/SFnGE8fJT3GXwEOngsV7Zt27NXFoaoApmYm81iuXoPkFOJwJ8ERdknLPMO" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/simplelightbox/1.14.0/simplelightbox.min.css" integrity="sha256-SKS4FkcNkeoVSuM7plVb7urk85SbYbpu13eMQa0XgcA=" crossorigin="anonymous" />
<link rel="stylesheet" type="text/css" href="{{ asset('assets/bolt.css') }}">
{% endblock %}
</head>
@@ -48,6 +49,7 @@
<script src="{{ asset('assets/bolt.js') }}"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.3/umd/popper.min.js" integrity="sha384-ZMP7rVo3mIykV+2+9J3UJ46jBk0WLaUAdn689aCwoqbBJiSnjAK/l8WvCWPIPm49" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.1.3/js/bootstrap.min.js" integrity="sha384-ChfqqxuZUCnJSK3+MXmPNIyE6ZbWh2IMqE241rYiqJxyMiZ6OW/JmZQ5stwEULTy" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/simplelightbox/1.14.0/simple-lightbox.min.js" integrity="sha256-QKuCZaHT3kehJCIS5bXUrC6ciUWfswFF7ylYYPWQ/9w=" crossorigin="anonymous"></script>
{% endblock %}
</body>

View File

@@ -3,13 +3,59 @@
{% block field %}
<label>{{ label }}</label>
<div class="inline field">
<label>Filename</label>
<input name="{{ name }}[filename]" placeholder="First Name" type="text" value="{{ field.get('filename') }}" class="{{ class }}" {{ attributes|default()|raw }}>
<div class="row" style="margin-right: 0;">
<div class="col-8">
<div class="row mb-2">
<div class="col-3">
<label>Filename</label>
</div>
<div class="col-9">
<input name="{{ name }}[filename]" id="{{ id }}-filename" placeholder="filename.jpg" type="text" value="{{ field.get('filename') }}" class="{{ class }}" {{ attributes|default()|raw }}>
</div>
</div>
<div class="row mb-2">
<div class="col-3">
<label>Alt</label>
</div>
<div class="col-9">
<input name="{{ name }}[alt]" placeholder="An image depicting a kitten" type="text" value="{{ field.get('alt') }}" class="{{ class }}" {{ attributes|default()|raw }}>
</div>
</div>
<div class="row mb-2">
<div class="col-3">
<label>Title</label>
</div>
<div class="col-9">
<input name="{{ name }}[alt]" placeholder="A description of this image" type="text" value="{{ field.get('title') }}" class="{{ class }}" {{ attributes|default()|raw }}>
</div>
</div>
<div class="row mb-2">
<div class="col-12" style="text-align: right;">
<a href="{{ field.get('filename')|thumbnail(width=1000, height=1000, area='files') }}" class="btn btn-secondary btn-small lightbox" style="color: #FFF;">view image</a>
<a href="" onclick="document.querySelector('.filepond--drop-label label').click(); return false;" class="btn btn-secondary btn-small" style="color: #FFF;">Upload an image</a>
</div>
</div>
</div>
<div class="col-4" id="{{ id }}-uploader" style="padding: 0; min-height: 180px;">
<file-pond name="filepond[]" ref="pond" accepted-file-types="image/jpeg, image/png" server="/async/upload"></file-pond>
</div>
</div>
<div class="inline field">
<label>Alt</label>
<input name="{{ name }}[alt]" placeholder="First Name" type="text" value="{{ field.get('alt') }}" class="{{ class }}" {{ attributes|default()|raw }}>
</div>
<hr>
<style>
#{{ id }}-uploader {
background-image: url({{ field.get('filename')|thumbnail(width=400, height=300, area='files') }});
background-repeat: no-repeat;
background-size: cover;
}
.filepond--root {
margin: 0;
}
</style>
{% endblock %}

View File

@@ -7,18 +7,19 @@
<span class="input-group-text">{{ field.slugPrefix }}</span>
</div>
<input name="{{ name }}" placeholder="…" type="text" value="{{ value }}" class="{{ class }}" readonly="readonly">
<div class="input-group-append">
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<i class="fas fa-unlock"></i> Locked
<div class="input-group-append" id="{{ id }}-dropdown">
<button class="btn btn-secondary dropdown-toggle" type="button" data-toggle="dropdown" aria-haspopup="true"
aria-expanded="false">
<i class="fas fa-unlock fa-fw"></i> Locked
</button>
<div class="dropdown-menu">
<a class="dropdown-item">
<a class="dropdown-item" data-action="lock">
<i class="fas fa-unlock fa-fw"></i> Locked
</a>
<a class="dropdown-item">
<a class="dropdown-item" data-action="generate">
<i class="fas fa-link fa-fw"></i> Generate from: {{ field.slugUseFields|join(', ') }}
</a>
<a class="dropdown-item">
<a class="dropdown-item" data-action="edit">
<i class="fas fa-pencil-alt fa-fw"></i> Edit
</a>
</div>

View File

@@ -0,0 +1,35 @@
{% extends '@bolt/editcontent/javascripts/_base.twig' %}
{% block javascripts %}
<script>
FilePond.registerPlugin(
FilePondPluginFileMetadata,
FilePondPluginImagePreview
);
FilePond.setOptions({
fileMetadataObject: {
'area': 'files',
'path': ''
}
});
new Vue({
el: '#{{ id}}-uploader',
components: {
FilePond: vueFilePond.default()
}
});
const pond = document.querySelector('.filepond--root');
pond.addEventListener('FilePond:processfile', e => {
document.querySelector("#{{ id }}-filename").value = e.detail.file.serverId;
});
pond.addEventListener('FilePond:addfile', e => {
document.querySelector('.filepond--wrapper').style.opacity = "0.5 !important";
console.log('foooo');
document.querySelector("#{{ id }}-uploader").style.background = 'none';
});
</script>
{% endblock %}

View File

@@ -0,0 +1,5 @@
<script src="https://unpkg.com/filepond-plugin-image-preview"></script>
<script src="https://unpkg.com/filepond-plugin-file-metadata/dist/filepond-plugin-file-metadata.js"></script>
<script src="https://unpkg.com/filepond"></script>
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vue-filepond"></script>

View File

@@ -0,0 +1,64 @@
{% extends '@bolt/editcontent/javascripts/_base.twig' %}
{% block javascripts %}
<script>
var input = document.querySelector("input[name=\"{{ name }}\"]");
var slugTick;
var element = document.querySelector('#{{ id }}-dropdown');
element.addEventListener('click', function (event) {
if (action = event.target.getAttribute('data-action')) {
var target = event.target;
} else if (action = event.target.parentElement.getAttribute('data-action')) {
var target = event.target.parentElement;
} else {
return;
}
document.querySelector('#{{ id }}-dropdown button').innerHTML = target.innerHTML;
clearTimeout(slugTick);
if (action == 'lock') {
input.readOnly = true;
}
if (action == 'generate') {
input.readOnly = true;
var fromfields = ["{{ field.slugUseFields|join("', '") }}"];
updateSlug(fromfields, input);
}
if (action == 'edit') {
input.readOnly = false;
input.select();
}
}, false);
function updateSlug(fromfields, input) {
var newSlug = '';
fromfields.forEach(function (field) {
newSlug = newSlug + document.querySelector("input[name=\"fields[" + field + "]\"]").value;
});
input.value = slugify(newSlug);
slugTick = setTimeout(function() {
updateSlug(fromfields, input);
}, 400);
}
function slugify(text)
{
return text.toString().toLowerCase()
.replace(/\s+/g, '-') // Replace spaces with -
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
.replace(/\-\-+/g, '-') // Replace multiple - with single -
.replace(/^-+/, '') // Trim - from start of text
.replace(/-+$/, ''); // Trim - from end of text
}
// If slug is empty, "click" the 'generate from' option.
if (input.value == '') {
document.querySelector("a[data-action=\"generate\"]").click();
}
</script>
{% endblock %}

View File

@@ -0,0 +1,2 @@
<link rel="stylesheet" href="https://unpkg.com/filepond/dist/filepond.min.css">
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css">

View File

@@ -24,7 +24,7 @@
{% set dimensions = '' %}
{% if extension in imageformats %}
{% set thumbnail = filename|thumbnail(100, 72) %}
{% set thumbnail = filename|thumbnail(width = 100, height = 72, area = area) %}
{% set icon = 'fa-image' %}
{% set link = path('bolt_media_new', {'area': area, 'file': filename}) %}
@@ -47,7 +47,9 @@
</td>
<td class="listing-thumb" style="padding: 0.2em;">
{%- if thumbnail -%}
<a href="{{ filename|thumbnail(width = 1000, height = 1000, area=area) }}" class="lightbox">
<img src="{{ thumbnail }}" width="100" height="72">
</a>
{%- else -%}
&nbsp;
{%- endif -%}

View File

@@ -0,0 +1,40 @@
<link rel="stylesheet" href="https://unpkg.com/filepond/dist/filepond.min.css">
<link rel="stylesheet" href="https://unpkg.com/filepond-plugin-image-preview/dist/filepond-plugin-image-preview.min.css">
<script src="https://unpkg.com/filepond-plugin-image-preview"></script>
<script src="https://unpkg.com/filepond-plugin-file-metadata/dist/filepond-plugin-file-metadata.js"></script>
<script src="https://unpkg.com/filepond"></script>
<script src="https://unpkg.com/vue"></script>
<script src="https://unpkg.com/vue-filepond"></script>
<div class="card mb-4">
<div class="card-header">File uploader</div>
<div id="uploader">
<file-pond name="filepond[]"
ref="pond"
allow-multiple="true"
accepted-file-types="image/jpeg, image/png"
server="/async/upload"></file-pond>
</div>
</div>
<script>
FilePond.registerPlugin(
FilePondPluginFileMetadata,
FilePondPluginImagePreview
);
FilePond.setOptions({
fileMetadataObject: {
'area': '{{ area }}',
'path': '{{ path }}'
}
});
new Vue({
el: '#uploader',
components: {
FilePond: vueFilePond.default()
}
});
</script>

View File

@@ -18,13 +18,16 @@
{% block aside %}
<div class="card">
<div class="card mb-4">
<div class="card-header">Meta information</div>
<div class="card-body">
</div>
</div>
{% include '@bolt/finder/_uploader.twig' %}
{% endblock aside %}
@@ -32,6 +35,11 @@
{{ parent() }}
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/selectize.js/0.12.6/css/selectize.min.css" integrity="sha256-EhmqrzYSImS7269rfDxk4H+AHDyu/KwV1d8FDgIXScI=" crossorigin="anonymous" />
<style>
.filepond--root {
margin-bottom: 0 !important;
}
</style>
{% endblock %}