Codeigniter 4 with drag/drop multiple image uploader, validation - codeigniter-4

I am using a drag/drop image uploader script with my Codeigniter 4 project and it works well and I can use standard CI4 rules for validating file size, type, etc. but I need to check the max number of files being uploaded. I tried creating a custom rule but the data array does not contain the files. How should this be handled?
Below is test code in my controller when the form is submitted.
public function test_do()
{
helper(['form','url']);
$validation = \Config\Services::validation();
$rules = ['business_images' => ['label' => 'business image(s)',
'rules' => 'validate_images[business_images]']
];
if(!$this->validate($rules))
{
$error = $validation->getError('business_images');
echo 'Error - >'.$error;
}
else
{
echo 'good';
}
}
The image rule class is as follows.
class ImageRules
{
public function validate_images($str, string $fields, array $data, ?string &$error = null): bool
{
if(count($data['business_Images']) > 2)
{
$error = 'Too many files';
return false;
}
return true;
}
}

Related

Silverstripe 4 PaginatedList get Page-number - Link (Backlink)

In Silverstripe 4 I have a
DataObject 'PublicationObject',
a 'PublicationPage' and
a 'PublicationPageController'
PublicationObjects are displayed on PublicationPage through looping a PaginatedList. There is also a Pagination, showing the PageNumbers and Prev & Next - Links.
The Detail of PublicationObject is shown as 'Dataobject as Pages' using the UrlSegment.
If users are on the Detail- PublicationObject i want them to get back to the PublicationPage (the paginated list) with a Back - Link.
Two situations:
The user came from PublicationPage
and
The User came from a Search - Engine (Google) directly to the
DataObject- Detail.
I have done this like so:
$parentPage = Page::get()->Filter(array('UrlSegment' => $request->param('pageID')))->First();
$back = $_SERVER['HTTP_REFERER'];
if ((isset($_SERVER['HTTP_REFERER']) && !empty($_SERVER['HTTP_REFERER']))) {
if (strtolower(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST)) != strtolower($_SERVER['HTTP_HOST'])) {
// referer not from the same domain
$back = $parentPage->Link();
}
}
Thats not satisfying.
Question:
How do i get the Pagination - Link ( e.g: ...publicationen?start=20 ) when we are on the Detail - DataObject? How can we find the Position of the current Dataobject in that paginatedList in correlation with the Items per Page? (The Page- Link This Dataobject is on)
<?php
use SilverStripe\Control\Controller;
use SilverStripe\Control\HTTPRequest;
use SilverStripe\View\Requirements;
use SilverStripe\Core\Convert;
use SilverStripe\SiteConfig\SiteConfig;
use SilverStripe\ORM\PaginatedList;
use SilverStripe\Control\Director;
use SilverStripe\ORM\DataObject;
use SilverStripe\ErrorPage\ErrorPage;
use SilverStripe\Dev\Debug;
use SilverStripe\Dev\Backtrace;
class PublicationPageController extends PageController
{
private static $allowed_actions = ['detail'];
private static $url_handlers = array(
);
public static $current_Publication_id;
public function init() {
parent::init();
}
public function detail(HTTPRequest $request)
{
$publication = PublicationObject::get_by_url_segment(Convert::raw2sql($request->param('ID')));
if (!$publication) {
return ErrorPage::response_for(404);
}
// HERE I WANT TO FIND THE POSITION OF THE DATAOBJECT IN THE PAGINATEDLIST OR RATHER THE PAGE - LINK THIS DATAOBJECT IS IN
//$paginatedList = $this->getPaginatedPublicationObjects();
//Debug::show($paginatedList->find('URLSegment', Convert::raw2sql($request->param('ID'))));
//Debug::show($paginatedList->toArray());
$parentPage = Page::get()->Filter(array('UrlSegment' => $request->param('pageID')))->First();
$back = $_SERVER['HTTP_REFERER'];
if ((isset($_SERVER['HTTP_REFERER']) && !empty($_SERVER['HTTP_REFERER']))) {
if (strtolower(parse_url($_SERVER['HTTP_REFERER'], PHP_URL_HOST)) != strtolower($_SERVER['HTTP_HOST'])) {
// referer not from the same domain
$back = $parentPage->Link();
}
}
static::$current_Publication_id = $publication->ID;
$id = $publication->ID;
if($publication){
$arrayData = array (
'Publication' => $publication,
'Back' => $back,
'SubTitle' => $publication->Title,
'MetaTitle' => $publication->Title,
);
return $this->customise($arrayData)->renderWith(array('PublicationDetailPage', 'Page'));
}else{
return $this->httpError(404, "Not Found");
}
}
public function getPaginatedPublicationObjects()
{
$list = $this->PublicationObjects()->sort('SortOrder');
return PaginatedList::create($list, $this->getRequest()); //->setPageLength(4)->setPaginationGetVar('start');
}
}
EDIT:
is there a more simple solution ? than this ? :
public function detail(HTTPRequest $request)
{
$publication = PublicationObject::get_by_url_segment(Convert::raw2sql($request->param('ID')));
if (!$publication) {
return ErrorPage::response_for(404);
}
//*******************************************************
// CREATE BACK - LINK
$paginatedList = $this->getPaginatedPublicationObjects();
$dO = PublicationObject::get();
$paginationVar = $paginatedList->getPaginationGetVar();
$sqlQuery = new SQLSelect();
$sqlQuery->setFrom('PublicationObject');
$sqlQuery->selectField('URLSegment');
$sqlQuery->setOrderBy('SortOrder');
$rawSQL = $sqlQuery->sql($parameters);
$result = $sqlQuery->execute();
$list = [];
foreach($result as $row) {
$list[]['URLSegment'] = $row['URLSegment'];
}
$list = array_chunk($list, $paginatedList->getPageLength(), true);
$start = '';
$back = '';
$i = 0;
$newArray = [];
foreach ($list as $k => $subArr) {
$newArray[$i] = $subArr;
unset($subArr[$k]['URLSegment']);
foreach ($newArray[$i] as $key => $val) {
if ($val['URLSegment'] === Convert::raw2sql($request->param('ID'))) {
$start = '?'.$paginationVar.'='.$i;
}
}
$i = $i + $paginatedList->getPageLength();
}
$back = Controller::join_links($this->Link(), $start);
// END CREATE BACK - LINK
//*****************************************************
static::$current_Publication_id = $publication->ID;
$id = $publication->ID;
if($publication){
$arrayData = array (
'Publication' => $publication,
'Back' => $back,
'MyLinkMode' => 'active',
'SubTitle' => $publication->Title,
'MetaTitle' => $publication->Title,
);
return $this->customise($arrayData)->renderWith(array('PublicationDetailPage', 'Page'));
}else{
return $this->httpError(404, "Not Found");
}
}
You can simply pass the page number as a get var in the URL from the PublicationPage. The detail() method can then grab the get var and if there's a value for the page number passed in, add it to the back link's URL.
In the template:
<% loop $MyPaginatedList %>
Click me
<% end_loop %>
In your controller's detail() method:
$pageNum = $request->getVar('onPage');
if ($pageNum) {
// Add the pagenum to the back link here
}
Edit
You've expressed that you want this to use the page start offset (so you can just plug it into the url using ?start=[offset]), and that you want an example that also covers people coming in from outside your site. I therefore propose the following:
Do as above, but using PageStart instead of CurrentPage - this will mean you don't have to re-compute the offset any time someone clicks the link from your paginated list.
If there is no onPage (or pageOffset as I've renamed it below, assuming you'll use PageStart) get variable, then assuming you can ensure your SortOrder values are all unique, you can check how many items are in the list before the current item - which gives you your item offset. From that you can calculate what page it's on, and what the page offset is for that page, which is your start value.
public function detail(HTTPRequest $request)
{
$publication = PublicationObject::get_by_url_segment(Convert::raw2sql($request->param('ID')));
$paginatedList = $this->getPaginatedPublicationObjects();
// Back link logic starts here
$pageOffset = $request->getVar('pageOffset');
if ($pageOffset === null) {
$recordOffset = $paginatedList->filter('SortOrder:LessThan', $publication->SortOrder)->count() + 1;
$perPage = $paginatedList->getPageLength();
$page = floor($recordOffset / $perPage) + 1;
$pageOffset = ($page - 1) * $perPage;
}
$back = $this->Link() . '?' . $paginatedList->getPaginationGetVar() . '=' . $pageOffset;
// Back link logic ends here
//.....
}

Laravel Excel upload and progressbar

I have a website where I can upload a .xlsx file which contains some rows of information for my database. I read the documentation for laravel-excel but it looks like it only works with progress bar if you use the console method; which I don't.
I currently just use a plain HTML upload form, no ajax yet.
But to create this progress bar for this I need to convert it to ajax, which is no hassle, that I can do.
But how would I create the progress bar when uploading the file and iterating through each row in the Excel file?
This is the controller and method where the upload gets done:
/**
* Import companies
*
* #param Import $request
* #return \Illuminate\Routing\Redirector|\Illuminate\Http\RedirectResponse
*/
public function postImport(Import $request)
{
# Import using Import class
Excel::import(new CompaniesImport, $request->file('file'));
return redirect(route('dashboard.companies.index.get'))->with('success', 'Import successfull!');
}
And this is the import file:
public function model(array $row)
{
# Don't create or validate on empty rows
# Bad workaround
# TODO: better solution
if (!array_filter($row)) {
return null;
}
# Create company
$company = new Company;
$company->crn = $row['crn'];
$company->name = $row['name'];
$company->email = $row['email'];
$company->phone = $row['phone'];
$company->website = (!empty($row['website'])) ? Helper::addScheme($row['website']) : '';
$company->save();
# Everything empty.. delete address
if (!empty($row['country']) || !empty($row['state']) || !empty($row['postal']) || !empty($row['address']) || !empty($row['zip'])) {
# Create address
$address = new CompanyAddress;
$address->company_id = $company->id;
$address->country = $row['country'];
$address->state = $row['state'];
$address->postal = $row['postal'];
$address->address = $row['address'];
$address->zip = $row['zip'];
$address->save();
# Attach
$company->addresses()->save($address);
}
return $company;
}
I know this is not much at this point. I just need some help figuring out how I would create this progress bar, because I'm pretty stuck.
My thought is to create a ajax upload form though, but from there I don't know.
Just an idea, but you could use the Laravel session to store the total_row_count and processed_row_count during the import execution. Then, you could create a separate AJAX call on a setInterval() to poll those session values (e.g., once per second). This would allow you to calculate your progress as processed_row_count / total_row_count, and output to a visual progress bar. – matticustard
Putting #matticustard comment into practice. Below is just sample of how things could be implemented, and maybe there are areas to improve.
1. Routes
import route to initialize Excel import.
import-status route will be used to get latest import status
Route::post('import', [ProductController::class, 'import']);
Route::get('import-status', [ProductController::class, 'status']);
2. Controller
import action will validate uploaded file, and pass $id to ProductsImport class. As it will be queued and run in the background, there is no access to current session. We will use cache in the background. It will be good idea to generate more randomized $id if more concurrent imports will be processed, for now just unix date to keep simple.
You currently cannot queue xls imports. PhpSpreadsheet's Xls reader contains some non-utf8 characters, which makes it impossible to queue.
XLS imports could not be queued
public function import()
{
request()->validate([
'file' => ['required', 'mimes:xlsx'],
]);
$id = now()->unix()
session([ 'import' => $id ]);
Excel::queueImport(new ProductsImport($id), request()->file('file')->store('temp'));
return redirect()->back();
}
Get latest import status from cache, passing $id from session.
public function status()
{
$id = session('import');
return response([
'started' => filled(cache("start_date_$id")),
'finished' => filled(cache("end_date_$id")),
'current_row' => (int) cache("current_row_$id"),
'total_rows' => (int) cache("total_rows_$id"),
]);
}
3. Import class
Using WithEvents BeforeImport we set total rows of our excel file to the cache. Using onRow we set currently processing row to the cache. And AfterReset clear all the data.
<?php
namespace App\Imports;
use App\Models\Product;
use Maatwebsite\Excel\Row;
use Maatwebsite\Excel\Concerns\OnEachRow;
use Maatwebsite\Excel\Events\AfterImport;
use Maatwebsite\Excel\Events\BeforeImport;
use Maatwebsite\Excel\Concerns\WithEvents;
use Illuminate\Contracts\Queue\ShouldQueue;
use Maatwebsite\Excel\Concerns\WithStartRow;
use Maatwebsite\Excel\Concerns\WithChunkReading;
use Maatwebsite\Excel\Concerns\WithMultipleSheets;
class ProductsImport implements OnEachRow, WithEvents, WithChunkReading, ShouldQueue
{
public $id;
public function __construct(int $id)
{
$this->id = $id;
}
public function chunkSize(): int
{
return 100;
}
public function registerEvents(): array
{
return [
BeforeImport::class => function (BeforeImport $event) {
$totalRows = $event->getReader()->getTotalRows();
if (filled($totalRows)) {
cache()->forever("total_rows_{$this->id}", array_values($totalRows)[0]);
cache()->forever("start_date_{$this->id}", now()->unix());
}
},
AfterImport::class => function (AfterImport $event) {
cache(["end_date_{$this->id}" => now()], now()->addMinute());
cache()->forget("total_rows_{$this->id}");
cache()->forget("start_date_{$this->id}");
cache()->forget("current_row_{$this->id}");
},
];
}
public function onRow(Row $row)
{
$rowIndex = $row->getIndex();
$row = array_map('trim', $row->toArray());
cache()->forever("current_row_{$this->id}", $rowIndex);
// sleep(0.2);
Product::create([ ... ]);
}
}
4. Front end
On the front-end side this is just sample how things could be handled. Here I used vuejs, ant-design-vue and lodash.
After uploading file handleChange method is called
On successful upload trackProgress method is called for the first time
trackProgress method is recursive function, calling itself on complete
with lodash _.debounce method we can prevent calling it too much
export default {
data() {
this.trackProgress = _.debounce(this.trackProgress, 1000);
return {
visible: true,
current_row: 0,
total_rows: 0,
progress: 0,
};
},
methods: {
handleChange(info) {
const status = info.file.status;
if (status === "done") {
this.trackProgress();
} else if (status === "error") {
this.$message.error(_.get(info, 'file.response.errors.file.0', `${info.file.name} file upload failed.`));
}
},
async trackProgress() {
const { data } = await axios.get('/import-status');
if (data.finished) {
this.current_row = this.total_rows
this.progress = 100
return;
};
this.total_rows = data.total_rows;
this.current_row = data.current_row;
this.progress = Math.ceil(data.current_row / data.total_rows * 100);
this.trackProgress();
},
close() {
if (this.progress > 0 && this.progress < 100) {
if (confirm('Do you want to close')) {
this.$emit('close')
window.location.reload()
}
} else {
this.$emit('close')
window.location.reload()
}
}
},
};
<template>
<a-modal
title="Upload excel"
v-model="visible"
cancel-text="Close"
ok-text="Confirm"
:closable="false"
:maskClosable="false"
destroyOnClose
>
<a-upload-dragger
name="file"
:multiple="false"
:showUploadList="false"
:action="`/import`"
#change="handleChange"
>
<p class="ant-upload-drag-icon">
<a-icon type="inbox" />
</p>
<p class="ant-upload-text">Click to upload</p>
</a-upload-dragger>
<a-progress class="mt-5" :percent="progress" :show-info="false" />
<div class="text-right mt-1">{{ this.current_row }} / {{ this.total_rows }}</div>
<template slot="footer">
<a-button #click="close">Close</a-button>
</template>
</a-modal>
</template>
<script>
export default {
data() {
this.trackProgress = _.debounce(this.trackProgress, 1000);
return {
visible: true,
current_row: 0,
total_rows: 0,
progress: 0,
};
},
methods: {
handleChange(info) {
const status = info.file.status;
if (status === "done") {
this.trackProgress();
} else if (status === "error") {
this.$message.error(_.get(info, 'file.response.errors.file.0', `${info.file.name} file upload failed.`));
}
},
async trackProgress() {
const { data } = await axios.get('/import-status');
if (data.finished) {
this.current_row = this.total_rows
this.progress = 100
return;
};
this.total_rows = data.total_rows;
this.current_row = data.current_row;
this.progress = Math.ceil(data.current_row / data.total_rows * 100);
this.trackProgress();
},
close() {
if (this.progress > 0 && this.progress < 100) {
if (confirm('Do you want to close')) {
this.$emit('close')
window.location.reload()
}
} else {
this.$emit('close')
window.location.reload()
}
}
},
};
</script>

Overwrite backend template in bolt.cms

I am trying to overwrite a template file located in vendor/bolt/bolt/app/view/twig/editcontent/fields/_block.twig (I want to replace the "block selection" dropdown). Regarding to #1173, #1269, #5588, #3768 and #5102 this is not supported by default so I have to write a extension for this. So I tried this:
BackendBlockSelectionExtension:
namespace Bundle\Site;
use Bolt\Filesystem\Adapter\Local;
use Bolt\Filesystem\Filesystem;
use Silex\Application;
use Bolt\Extension\SimpleExtension;
class BackendBlockSelectionExtension extends SimpleExtension
{
public function getServiceProviders()
{
return [
$this,
new BackendBlockSelectionProvider(),
];
}
}
BackendBlockSelectionProvider:
namespace Bundle\Site;
use Bolt\Filesystem\Adapter\Local;
use Bolt\Filesystem\Filesystem;
use Silex\Application;
use Silex\ServiceProviderInterface;
class BackendBlockSelectionProvider implements ServiceProviderInterface
{
public function register(Application $app)
{
$side = $app['config']->getWhichEnd();
if ($side == 'backend') {
$path = __DIR__ . '/App/templates/Backend';
$filesystem = $app['filesystem'];
$filesystem->mountFilesystem('bolt', new Filesystem(new Local($path)));
$app['twig.loader.bolt_filesystem'] = $app->share(
$app->extend(
'twig.loader.bolt_filesystem',
function ($filesystem, $app) {
$path = __DIR__ . 'src/App/templates/Backend/';
$filesystem->prependPath($path, 'bolt');
return $filesystem;
}
)
);
}
}
public function boot(Application $app)
{
}
}
This seems to do the job, but I got an error I don't understand at all: The "bolt://app/theme_defaults" directory does not exist.
So my final question is: Does anyone have some example code how to overwrite/modify vendor/bolt/bolt/app/view/twig/editcontent/fields/_block.twig without touching the vendor folder?
This should be much simplier than this.
In your extension class overwrite protected function registerTwigPaths() function like this:
protected function registerTwigPaths()
{
if ($this->getEnd() == 'backend') {
return [
'view' => ['position' => 'prepend', 'namespace' => 'bolt']
];
}
return [];
}
private function getEnd()
{
$backendPrefix = $this->container['config']->get('general/branding/path');
$end = $this->container['config']->getWhichEnd();
switch ($end) {
case 'backend':
return 'backend';
case 'async':
// we have async request
// if the request begin with "/admin" (general/branding/path)
// it has been made on backend else somewhere else
$url = '/' . ltrim($_SERVER['REQUEST_URI'], $this->container['paths']['root']);
$adminUrl = '/' . trim($backendPrefix, '/');
if (strpos($url, $adminUrl) === 0) {
return 'backend';
}
default:
return $end;
}
}
Now you can crete a view directory in your extensions directory in which you can define templates in structure like in Bolt's default. I would start with copy and overwrite.

Unable to access an error message corresponding to your field name -while uploading file in fuel cms in MY_Form_validation

I have written an validation rule on MY_Form_validation class under application -> library folder
as follow ..
public function doc_uploaded($field,$types)
{
$types=explode(',',$types);
$upload = $this->CI->lang->line('upload');
if(isset($_FILES[$field]) && !empty($_FILES[$field]['name']))
{
if(!function_exists('file_upload')){ $this->CI->load->helper('MY_file_helper'); }
$status = file_upload(($field),$_FILES[$field], captcha_path(),$types);
if($status >0 or strlen($status) > 2 )
{
return TRUE;
}
elseif(is_array($status))
{
$this->CI->_error_array[$field] = $status[0];
$this->CI->_error_array[$field] = $status[0];
$this->_error_array[$field]=sprintf( $upload,$this->_translate_fieldname($this->_field_data[$field]['label']));
// echo $field, print_r($this->_error_array);
// print_r($this->CI->_error_array);die();
return FALSE;
}
else
{
$this->_error_array[$field] = $status[0];
return FALSE;
}
}
else
{
return FALSE;
}
}
i am setting the validation rule from controller as follow
$this->form_validation->set_rules('resume', 'Resume', 'required|doc_uploaded[pdf,doc,docx,odt,txt,ppt,pptx]');
$_POST['resume'] ='resume';
now this one working fine except that error massage :
Unable to access an error message corresponding to your field name
Resume.(doc_uploaded)
must be an way to fix it please help . iam using 1.4
instead of
$this->_error_array[$field] = $status[0];
using
$this->CI->form_validation->set_message('doc_uploaded',$status[0]);
resolved the issue

kohana 3 incorporate file validate in orm

I cant figure out how to add file validation code to the orm model
As the code is now the text inputs are validated in the orm, but the file has no validation on it at all. I was wondering if it was possible to merge the file validation in the 'profile' model where I have the text validation?
Here is the code in my controller
public function action_public_edit()
{
$content = View::factory('profile/public_edit')
->bind('user', $user)
->bind('profile', $profile)
->bind('errors', $errors);
$user = Auth::instance()->get_user();
$profile = $user->profiles;
$this->template->content = $content;
if ($_POST)
{
if ($profile->values($_POST)->check())
{
$picture = Upload::save($_FILES['profile_picture']);
// Resize, sharpen, and save the image
Image::factory($picture)
->resize(100, 100, Image::WIDTH)
->save();
$profile->profile_picture = basename($picture);
$profile->save();
$redirect = "profile/private";
Request::instance()->redirect($redirect);
}
else
{
$errors = $profile->validate()->errors('profile/public_edit');
}
}
}
And the ORM model
protected $_belongs_to = array('user' => array());
protected $_rules = array(
'first_name' => array(
'not_empty' => NULL,
),
'last_name' => array(
'not_empty' => NULL,
),
You don't need to check() if you save() later - save will do the checking and throw ORM_Validation_Exception if validation is not passed. I suggest to create a function in your model for saving - move the lines after check() to this model. In controller you'll only need to write:
if ($this->request->method() === 'POST')
{
$errors = $profile->upload('profile_picture');
if (empty($errors))
{
try
{
$profile->your_save_function();
$this->request->redirect('profile/private');
}
catch (ORM_Validation_Exception $e)
{
$errors = $e->errors('profile');
}
}
}
In your model:
public function your_save_function()
{
// Do your stuff before uploading the picture
// Save the model
$this->save();
}
public function upload($filename)
{
// Set the default upload directory
Upload::$default_directory = 'your_path';
// Validate the file first
$validation = Validation::factory($_FILES)
->rules($filename, array(
array('not_empty'),
array('Upload::not_empty'),
array('Upload::valid'),
array('Upload::type', array(':value', array('your_valid_extensions'))),
array('Upload::size', array(':value', 'your_max_size')),
));
// Validate and upload OK
if ($validation->check())
{
$picture = Upload::save($_FILES[$filename]);
// Resize, sharpen, and save the image
Image::factory($picture)
->resize(100, 100, Image::WIDTH)
->save();
$this->profile_picture = basename($picture);
}
else
{
return $validation->errors('upload');
}
}
Much simpler and cleaner code.

Resources