Commit eb289e17 authored by Malcolm Blaney's avatar Malcolm Blaney

Added support for uploading multiple files in Browser module. Fixed

a bug in the Purchase module that allowed saving old order data in
purchase mode. Changed leaflet.css z-index values because it was
causing issues with other z-index values used by Control module and
jQuery UI. Fixed a bug in SimplePie Item class that meant image
cache was being used for any url. Added support for file uploads to
micropub endpoint.
parent dea75ddd
Pipeline #37860555 passed with stage
in 1 minute and 22 seconds
......@@ -32,7 +32,7 @@ class Browser extends Base {
return $this->RemoveFile();
}
if ($us_action === 'upload') {
return $this->UploadFile();
return $this->Upload();
}
if ($us_action === 'rotate') {
return $this->RotateImage();
......@@ -58,7 +58,8 @@ class Browser extends Base {
$content = '<form id="browser-upload-form">' .
'<div class="form-spacing">' .
'<label for="browser-upload-input">Upload a file:</label>' .
'<input id="browser-upload-input" name="upload" type="file">' .
'<input id="browser-upload-input" name="file[]" type="file" ' .
'multiple>' .
'<button id="browser-upload">upload</button>' .
'</div>' .
'<div id="browser-upload-progress"></div>' .
......@@ -117,7 +118,7 @@ class Browser extends Base {
}
public function Factory($fn, $p = NULL) {
if ($fn === 'Upload') return $this->Upload();
}
public function Group() {
......@@ -190,26 +191,54 @@ class Browser extends Base {
return ['error' => 'Filename does not have correct format.'];
}
private function UploadFile() {
private function Upload() {
// First check if the user's upload directory is at capacity.
$handle = popen('/usr/bin/du -sm ' . $this->PublicDirectory(), 'r');
$size = fgets($handle);
pclose($handle);
if (!preg_match('/^([0-9]+)/', $size, $match)) {
$remaining = 0;
if (preg_match('/^([0-9]+)/', $size, $match)) {
$remaining = $this->user->config->MaxUpload() - (int)$match[1];
}
else {
return ['error' => 'Could not check upload directory'];
}
$size = $match[1];
if ((int)$size > $this->user->config->MaxUpload()) {
if ($remaining <= 0) {
return ['error' => 'Upload directory full.'];
}
if (!isset($_FILES['file']['size'])) {
return ['error' => 'Could not check uploaded file size.'];
}
if (is_array($_FILES['file']['size'])) {
$content = '';
for ($i = 0; $i < count($_FILES['file']['size']); $i++) {
$result = $this->UploadFile($_FILES['file']['name'][$i],
$_FILES['file']['tmp_name'][$i],
$_FILES['file']['size'][$i], $remaining);
if (isset($result['error'])) return $result;
$content .= $result['content'];
$remaining -= (int)$_FILES['file']['size'][$i] / 1000000;
}
return ['content' => $content];
}
return $this->UploadFile($_FILES['file']['name'],
$_FILES['file']['tmp_name'],
$_FILES['file']['size'], $remaining);
}
private function UploadFile($filename, $tmp, $size, $remaining) {
if (!isset($size)) {
return ['error' => 'Could not check uploaded file size.'];
}
$max_file_size = $this->user->config->MaxFileSize();
// ['upload']['size'] is given in bytes, MaxFileSize is in megabytes.
if ($_FILES['upload']['size'] > $max_file_size * 1000000) {
// size is in bytes, MaxFileSize is in megabytes.
if ((int)$size > $max_file_size * 1000000) {
return ['error' => 'Upload file is too large. (max ' .
$max_file_size . 'M)'];
}
// Replace spaces in the uploaded file name.
$filename = preg_replace('/ /', '_', basename($_FILES['upload']['name']));
$filename = preg_replace('/ /', '_', basename($filename));
$regex = '/^([a-z0-9_-]{1,200})\.([a-z0-9]{1,10})$/i';
if (!preg_match($regex, $filename, $match)) {
return ['error' => 'Filename does not have correct format.'];
......@@ -226,15 +255,14 @@ class Browser extends Base {
if (!in_array(strtolower($type), $allowed)) {
return ['error' => 'File type not allowed.'];
}
$tmp = $_FILES['upload']['tmp_name'];
if (!move_uploaded_file($tmp, $path)) {
return ['error' => 'File: ' . $filename . ' was not uploaded.'];
}
// If this user has a media endpoint configured transfer the file to it.
if (isset($this->user->settings['micropub']['config'])) {
$config = json_decode($this->user->settings['micropub']['config'], true);
if (isset($config['media-endpont'])) {
return $this->TransferMedia($path, $config['media-endpont']);
if (isset($config['media-endpoint'])) {
return $this->TransferMedia($path, $config['media-endpoint']);
}
}
......@@ -282,6 +310,7 @@ class Browser extends Base {
}
return ['content' =>
$this->ImageContent($thumbnail, $html_path . $filename)];
}
private function ImageContent($thumbnail, $filename, $local = true) {
......@@ -324,7 +353,7 @@ class Browser extends Base {
curl_setopt($ch, CURLOPT_HEADERFUNCTION,
function($ch, $header) use(&$location) {
if (preg_match('/^Location:(.+)$/', $header, $match)) {
$location = $match[1];
$location = trim($match[1]);
}
return strlen($header);
});
......@@ -350,7 +379,8 @@ class Browser extends Base {
return ['error' => 'Media endpoint did not return location.'];
}
if (strpos($location, 'http') !== 0) {
return ['error' => 'Media endpoint did not return a URL.'];
return ['error' =>
'Media endpoint did not return a URL: (' . $location . ')'];
}
$thumbnail = '';
if (in_array($mime_type, ['image/gif', 'image/jpeg', 'image/png'])) {
......
......@@ -1059,8 +1059,8 @@ class Groupwizard extends Base {
'<div class="form-spacing">' .
'<label for="groupwizard-invoice-look-forward">Invoices are sent ' .
'out <b>before</b> the order is collected:</label>' .
'<input for="groupwizard-invoice-look-forward" type="checkbox" ' .
'id="groupwizard-invoice-look-forward">' .
'<input type="checkbox" id="groupwizard-invoice-look-forward" ' .
$this->Checked('invoice-look-forward') . '>' .
'</div>' .
'<p>If the day invoices are sent is different from when purchases are '.
'collected, enter the number of days here between sending the ' .
......
......@@ -663,7 +663,7 @@ class Post extends Base {
$us_enclosure = '';
if (isset($us_content['enclosure'])) {
if ($micropub) {
$us_content['properties']['photo'] = $us_content['enclosure'];
$us_data['properties']['photo'] = $us_content['enclosure'];
}
else {
$us_enclosure = json_encode($us_content['enclosure']);
......
......@@ -117,6 +117,7 @@ class Purchase extends Base {
// Need to know when the purchase-group session variable is changed by
// a different page so that we can refresh.
$_SESSION['purchase-group-changed'] = false;
$_SESSION['purchase-order-check'] = false;
// refresh is checked by the 'time' callback.
if (isset($_GET['refresh']) && $_GET['refresh'] === 'true') {
......@@ -127,7 +128,17 @@ class Purchase extends Base {
$set_order_mode = true;
}
// Otherwise check if today is co-op-day, order mode will be used if not.
else if (!$this->PurchasingAvailable($co_op_day_text)) {
else if ($this->PurchasingAvailable($co_op_day_text)) {
// When the page is first loaded, the browser will check if stored
// purchase data should be saved. This causes problems when purchasing
// is now available but the browser still has an unsaved order stored.
// This data was never added to the group order, so shouldn't be saved
// now, but it's not possible to differentiate it from normal data. To
// avoid saving it set 'purchase-order-check' so that the time check
// can tell the browser to ignore it.
$_SESSION['purchase-order-check'] = true;
}
else {
$set_order_mode = true;
}
......@@ -2404,9 +2415,13 @@ class Purchase extends Base {
}
private function CurrentTime() {
if ($_SESSION['purchase-group-changed'] === true) {
if ($_SESSION['purchase-group-changed']) {
return ['error' => 'Session expired: reloading page.'];
}
if ($_SESSION['purchase-order-check']) {
$_SESSION['purchase-order-check'] = false;
return ['save' => false];
}
$default_group = $this->user->group;
if (isset($_SESSION['purchase-group'])) {
......@@ -2507,7 +2522,7 @@ class Purchase extends Base {
}
private function ListProducts() {
if ($_SESSION['purchase-group-changed'] === true) {
if ($_SESSION['purchase-group-changed']) {
return ['error' => 'Session expired: reloading page.'];
}
......@@ -2724,7 +2739,7 @@ class Purchase extends Base {
if (!$this->user->active) {
return ['error' => 'Inactive account.'];
}
if ($_SESSION['purchase-group-changed'] === true) {
if ($_SESSION['purchase-group-changed']) {
return ['error' => 'Session expired: reloading page.'];
}
......@@ -2805,7 +2820,7 @@ class Purchase extends Base {
if (!$this->user->active) {
return ['error' => 'Inactive account.'];
}
if ($_SESSION['purchase-group-changed'] === true) {
if ($_SESSION['purchase-group-changed']) {
return ['error' => 'Session expired: reloading page.'];
}
if (!isset($_POST['timestamp'])) {
......
......@@ -40,6 +40,7 @@ function filter(e,ui){var filenames=[];$.each(ui.content,function(i,data){filena
function filterReset(e,ui){if($('#browser-search').val()===''){$('.browser .filename').parent().show();}}
function upload(){var formData=new FormData();if(!formData){dobrado.log('Your browser doesn\'t support file uploading.','error');return;}
if($('#browser-upload-input').val()===''){$('#browser-upload').button({disabled:true});return;}
$('#browser-upload').button({disabled:false});formData.append('upload',$('#browser-upload-input').get(0).files[0]);formData.append('request','browser');formData.append('action','upload');formData.append('url',location.href);formData.append('token',dobrado.token);$.ajax({url:'/php/request.php',data:formData,contentType:false,processData:false,type:'POST',xhr:function(){var xhr=new XMLHttpRequest();xhr.upload.addEventListener('progress',function(e){if(e.lengthComputable){var percent=e.loaded/e.total*100;if(percent<100){$('#browser-upload-progress').show();$('#browser-upload-progress').progressbar('value',percent);}}});return xhr;},success:function(response){if(dobrado.checkResponseError(response,'browser upload')){return;}
var browser=JSON.parse(response);$('#browser-upload-form').after(browser.content);$('.thumbnail').click(highlight);$('.thumbnail .select:first').button({icon:'ui-icon-plus',showLabel:false}).click(select);$('.thumbnail .rotate-left:first').button({icon:'ui-icon-arrowreturnthick-1-w',showLabel:false}).click(rotateLeft);$('.thumbnail .rotate-right:first').button({icon:'ui-icon-arrowreturnthick-1-e',showLabel:false}).click(rotateRight);$('.thumbnail .remove:first').button({icon:'ui-icon-trash',showLabel:false}).click(remove);$('#browser-upload-input').val('');$('#browser-upload').button({disabled:true});$('#browser-upload-progress').hide();}});return false;}
$('#browser-upload').button({disabled:false});let fileList=$('#browser-upload-input').get(0).files;for(let i=0;i<fileList.length;i++){formData.append('file[]',fileList[i]);}
formData.append('request','browser');formData.append('action','upload');formData.append('url',location.href);formData.append('token',dobrado.token);$.ajax({url:'/php/request.php',data:formData,contentType:false,processData:false,type:'POST',xhr:function(){var xhr=new XMLHttpRequest();xhr.upload.addEventListener('progress',function(e){if(e.lengthComputable){var percent=e.loaded/e.total*100;if(percent<100){$('#browser-upload-progress').show();$('#browser-upload-progress').progressbar('value',percent);}}});return xhr;},success:function(response){if(dobrado.checkResponseError(response,'browser upload')){return;}
var browser=JSON.parse(response);$('#browser-search').parent().after(browser.content);$('.thumbnail').click(highlight);$('.thumbnail .select').button({icon:'ui-icon-plus',showLabel:false}).click(select);$('.thumbnail .rotate-left').button({icon:'ui-icon-arrowreturnthick-1-w',showLabel:false}).click(rotateLeft);$('.thumbnail .rotate-right').button({icon:'ui-icon-arrowreturnthick-1-e',showLabel:false}).click(rotateRight);$('.thumbnail .remove').button({icon:'ui-icon-trash',showLabel:false}).click(remove);$('#browser-upload-input').val('');$('#browser-upload').button({disabled:true});$('#browser-upload-progress').hide();}});return false;}
dobrado.browser.newModuleCallback=function(selector,context){callback=context;if(/^[0-9]+$/.test(callback)){setTimeout(function(){$(selector).parents('.ui-dialog').css('z-index',11000);},100);}};}());
\ No newline at end of file
......@@ -178,7 +178,10 @@ if (!this.dobrado.browser) {
}
$('#browser-upload').button({ disabled: false });
formData.append('upload', $('#browser-upload-input').get(0).files[0]);
let fileList = $('#browser-upload-input').get(0).files;
for (let i = 0; i < fileList.length; i++) {
formData.append('file[]', fileList[i]);
}
formData.append('request', 'browser');
formData.append('action', 'upload');
formData.append('url', location.href);
......@@ -207,21 +210,21 @@ if (!this.dobrado.browser) {
return;
}
var browser = JSON.parse(response);
$('#browser-upload-form').after(browser.content);
$('#browser-search').parent().after(browser.content);
$('.thumbnail').click(highlight);
$('.thumbnail .select:first').button({
$('.thumbnail .select').button({
icon: 'ui-icon-plus',
showLabel: false
}).click(select);
$('.thumbnail .rotate-left:first').button({
$('.thumbnail .rotate-left').button({
icon: 'ui-icon-arrowreturnthick-1-w',
showLabel: false
}).click(rotateLeft);
$('.thumbnail .rotate-right:first').button({
$('.thumbnail .rotate-right').button({
icon: 'ui-icon-arrowreturnthick-1-e',
showLabel: false
}).click(rotateRight);
$('.thumbnail .remove:first').button({
$('.thumbnail .remove').button({
icon: 'ui-icon-trash',
showLabel: false
}).click(remove);
......
......@@ -35,7 +35,8 @@ else{sync();}});function decimalString(value){return(value+0.0001).toFixed(2);}
function purchaseDataStored(){if(purchase.data){for(var user in purchase.data){if(purchase.data[user].length!==0){return true;}}}
return false;}
function sync(){dobrado.log('Requesting server time...','info');$.post('/php/request.php',{request:'purchase',action:'time',timestamp:purchase.date,url:location.href,token:dobrado.token},function(response){if(dobrado.checkResponseError(response,'purchase sync')){return;}
var current=JSON.parse(response);if(purchase.date<current.update||purchase.date<current.time-oneDay||purchase.group!==current.group||purchase.username!==current.username){if(current.save&&purchase.processed&&purchase.processed.length!==0){stockUpdate=current.update;yesterday=current.time-oneDay;save();}
var current=JSON.parse(response);if(!current.save){loadProducts();}
else if(purchase.date<current.update||purchase.date<current.time-oneDay||purchase.group!==current.group||purchase.username!==current.username){if(purchase.processed&&purchase.processed.length!==0){stockUpdate=current.update;yesterday=current.time-oneDay;save();}
else{loadProducts();}}
else{tomorrow=current.time+oneDay;purchase.date=current.time;if(dobrado.localStorage){localStorage.purchase=JSON.stringify(purchase);}
$('.purchase-date-info').html(purchase.dateInfo);var source=[];$.each(purchase.users,function(i,user){if(purchase.details[user]&&!purchase.details[user].supplierOnly){source.push(user);}});$('#purchase-name-input').autocomplete({minLength:1,search:dobrado.fixAutoCompleteMemoryLeak,source:source,select:showUser});updateProducts();if(purchase.processed&&purchase.processed.length!==0){$('.purchase .message .stored').html('You have data stored.');$('.purchase .save').button('option','disabled',false);dobrado.account.preventLogout=true;alert('The product list needs to be updated.\n'+'Please submit your purchases and then reload the page.');}}});}
......
......@@ -161,7 +161,8 @@ if (!this.dobrado.purchase) {
}
// Check if the local data has been saved. If it has then all data
// should be refreshed when the page is loaded. (If there's no data,
// might not have to reload and can go to sync instead).
// might not have to reload and can go to sync instead). Note that
// 'processed' means entered but not saved here...
if (!purchase || (purchase.processed && purchase.processed.length === 0 &&
purchaseDataStored())) {
loadProducts();
......@@ -204,6 +205,12 @@ if (!this.dobrado.purchase) {
return;
}
var current = JSON.parse(response);
// The current time checks below only make sense for purchase mode.
// current.save is set to false when ordering is no longer available,
// so check that first.
if (!current.save) {
loadProducts();
}
// The server returns a timestamp for when the stock table was last
// updated. If it's newer than our existing purchase.date need to
// refresh all data from the server.
......@@ -211,14 +218,13 @@ if (!this.dobrado.purchase) {
// The last case is that data is also refreshed when ordering, but in
// this case the purchase.date is set in the future. To reset need to
// check that the username doing the ordering or group has changed.
if (purchase.date < current.update ||
purchase.date < current.time - oneDay ||
purchase.group !== current.group ||
purchase.username !== current.username) {
else if (purchase.date < current.update ||
purchase.date < current.time - oneDay ||
purchase.group !== current.group ||
purchase.username !== current.username) {
// If there are orders that have already been processed, save them
// before refreshing stock data. Ordering must still be available.
if (current.save &&
purchase.processed && purchase.processed.length !== 0) {
// before refreshing stock data.
if (purchase.processed && purchase.processed.length !== 0) {
stockUpdate = current.update;
yesterday = current.time - oneDay;
save();
......
This diff is collapsed.
......@@ -43,6 +43,11 @@ function log_db($us_message, $user = '', $visitor = '', $page = '') {
$mysqli->close();
}
// This is a helper function for development. Don't leave calls to it in code.
function dobrado_error_log($error) {
error_log(print_r($error, true));
}
function dobrado_mail($email, $subject, $message, $headers,
$bcc, $sender, $user, $module, $type) {
// The default is send unless the user selects to stop sending mail.
......
......@@ -639,7 +639,7 @@ class SimplePie_Item
}
if (isset($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data']))
{
$uri = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]), true);
$uri = $this->sanitize($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['uri'][0]));
}
if (isset($author['child'][SIMPLEPIE_NAMESPACE_ATOM_10]['email'][0]['data']))
{
......@@ -661,7 +661,7 @@ class SimplePie_Item
}
if (isset($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data']))
{
$url = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]), true);
$url = $this->sanitize($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]['data'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['url'][0]));
}
if (isset($author[0]['child'][SIMPLEPIE_NAMESPACE_ATOM_03]['email'][0]['data']))
{
......@@ -2835,15 +2835,17 @@ class SimplePie_Item
$url = null;
$width = null;
$url = $this->sanitize($link['attribs']['']['href'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($link), true);
$try_cache = false;
if (isset($link['attribs']['']['type']))
{
$type = $this->sanitize($link['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT);
$try_cache = strpos($type, 'image/') === 0;
}
if (isset($link['attribs']['']['length']))
{
$length = ceil($link['attribs']['']['length']);
}
$url = $this->sanitize($link['attribs']['']['href'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($link), $try_cache);
// Since we don't have group or content for these, we'll just pass the '*_parent' variables directly to the constructor
$this->data['enclosures'][] = $this->registry->create('Enclosure', array($url, $type, $length, null, $bitrate, $captions_parent, $categories_parent, $channels, $copyrights_parent, $credits_parent, $description_parent, $duration_parent, $expression, $framerate, $hashes_parent, $height, $keywords_parent, $lang, $medium, $player_parent, $ratings_parent, $restrictions_parent, $samplingrate, $thumbnails_parent, $title_parent, $width));
......@@ -2870,15 +2872,17 @@ class SimplePie_Item
$url = null;
$width = null;
$url = $this->sanitize($enclosure[0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($enclosure[0]), true);
$try_cache = false;
if (isset($enclosure[0]['attribs']['']['type']))
{
$type = $this->sanitize($enclosure[0]['attribs']['']['type'], SIMPLEPIE_CONSTRUCT_TEXT);
$try_cache = strpos($type, 'image/') === 0;
}
if (isset($enclosure[0]['attribs']['']['length']))
{
$length = ceil($enclosure[0]['attribs']['']['length']);
}
$url = $this->sanitize($enclosure[0]['attribs']['']['url'], SIMPLEPIE_CONSTRUCT_IRI, $this->get_base($enclosure[0]), $try_cache);
// Since we don't have group or content for these, we'll just pass the '*_parent' variables directly to the constructor
$this->data['enclosures'][] = $this->registry->create('Enclosure', array($url, $type, $length, null, $bitrate, $captions_parent, $categories_parent, $channels, $copyrights_parent, $credits_parent, $description_parent, $duration_parent, $expression, $framerate, $hashes_parent, $height, $keywords_parent, $lang, $medium, $player_parent, $ratings_parent, $restrictions_parent, $samplingrate, $thumbnails_parent, $title_parent, $width));
......
......@@ -277,9 +277,22 @@ else {
}
}
}
// If this is multipart/form-data then $_FILES will be set with name="file".
if (isset($_FILES['file'])) {
$browser = new Module($user, $owner, 'browser');
if (!$browser->IsInstalled()) {
header('HTTP/1.1 500 Internal Server Error');
log_db('micropub.php 6: Browser module is not installed.');
exit;
}
$result = $browser->Factory('Upload');
if (isset($result['error'])) log_db('micropub.php 7: ' . $result['error']);
}
}
if (count($content) === 0) {
log_db('micropub.php 6: No content found.');
log_db('micropub.php 7: No content found.');
header('HTTP/1.1 400 Bad Request');
exit;
}
......
......@@ -230,7 +230,7 @@ class Control extends Base {
$site_style = ['"",".control","position","sticky"',
'"",".control","width","100%"',
'"",".control","top","0"',
'"",".control","z-index","200"',
'"",".control","z-index","50"',
'"",".control","height","48px"',
'"",".control","display","none"',
'"",".control .wrapper","height","38px"',
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment