Concurrency issue when updating Wordpress metadata via ajax - php

Here's the scenario: I'm building a Wordpress plugin to manage some survey research that I'm involved in. The project admin is able to upload a csv file from the WP admin interface. On the client side, when the file is uploaded it goes through each line of the file, extracts the necessary info about the users and then makes an AJAX call to add the participant to the project. I decided to parse the csv file on the client side and submit the ajax requests one by one so that I could update a progress bar as each returns. The javascript looks like this:
$( '#csv_upload_button' ).click( function() {
// declare the necessary variables
var f = $( '#csv_file_input' )[0].files[0],
fr = new FileReader,
rows, headers, dialog, count, remaining;
// when the file loads
fr.onload = function() {
// get the rows, the count, number remaining to process, and headers
rows = fr.result.split( "\n" );
remaining = count = rows.length - 1; // -1 to account for header row
headers = $.trim( rows[0] ).split( ',' );
// create the dialog box to show the progress bar
dialog = $( '<div></div>' )
.html(
'<p>Loading...</p>' +
'<p><progress id="csv_upload_progress" max="' + count +
'" min="0" value="0"></p>' )
.dialog( { modal: true; } );
// then for each row in the file
$( rows ).each( function( i, r ) {
// create an object to hold the data
var data = {}, row = $.trim( r ).split( ',' ), j;
if ( i > 0 ) { // data starts on the second row
// map the data into our object
for ( j = 0; j < headers.length; j++ ) {
data[ headers[ j ] ] = row[ j ];
}
// send it to the server
$.post(
ajaxurl,
{
action: 'import_panel_member',
data: data,
postid: $( '#post_ID' ).val()
},
function( result ) {
var prog = $( '#csv_upload_progress' );
prog.attr( 'value', prog.attr( 'value' ) + 1 );
if ( 0 == --remaining ) {
// stuff to do when everything has been loaded
}
}
);
}
});
};
// read the csv file
fr.readAsText( f );
});
The PHP looks something like this:
function import_panel_member() {
header( 'content-type: application/json' );
// get the variables sent from the client
$postid = $_POST[ 'postid' ];
$data = $_POST[ 'data' ];
/*
* ...do other things involving talking to a 3rd party server...
*/
// get the WP meta data variable to be updated
$participants = get_post_meta( $postid, '_project_participants', true );
// modify it
$participants[] = $data;
// update the database
update_post_meta( $postid, '_project_participants', $participants );
// return a message to the client
echo json_encode( (object) array( 'success' => 1, 'message' => 'added' ) );
exit;
}
The problem is that since these requests happen asynchronously, it appears that the _project_participants metadata field is only being updated by the last record to be processed. In other words, only the last person in the list shows up in the list of participants. Here are some things that I've tried:
Change $.post() to $.ajax() and set async: false
This works but it's much slower (due to synchronous calls) and for some reason it prevents my dialog box from showing up until after all the ajax calls have finished.
Upload the entire csv file to the server and deal with it there
Instead of parsing the csv on the client side. This works, too, but I don't think I can get intermediate feedback from the server that I can use to update the progress bar. This request can take quite a while, and I don't want users to "give up" on the request before it's complete. When doing it this way, sometimes the server never responds to the ajax call.
So perhaps I am greedy and just want my cake and to eat it, too. How can I take advantage of the speed of asynchronous requests, which give me the opportunity to give the user feedback with a progress bar, but not screw myself up with concurrency issues on the server?

I figured it out. The answer was a hybrid of the two methods. I can use a series of $.post() calls to do the stuff that works better in async mode, and then upload the whole csv to do the stuff that works better in sync mode. Never would have figured this out without typing out the whole explanation in SO!

Related

Database Array Exploding

I have this row in a database
and I want to be able to change the values in each block (e.g. [license_civ_driver,1]) through a page on a website. The full cell in the database can be seen here.
"[[`license_civ_driver`,1],[`license_civ_boat`,0],[`license_civ_pilot`,1],[`license_civ_advpilot`,1],[`license_civ_trucking`,1],[`license_civ_gun`,1],[`license_civ_hunting`,0],[`license_civ_dive`,0],[`license_civ_home`,0],[`license_civ_platinum`,1],[`license_civ_oil`,1],[`license_civ_diamond`,0],[`license_civ_salt`,0],[`license_civ_sand`,0],[`license_civ_iron`,0],[`license_civ_copper`,1],[`license_civ_cement`,0],[`license_civ_rubber`,0],[`license_civ_meth`,0],[`license_civ_cocaine`,0],[`license_civ_heroin`,0],[`license_civ_marijuana`,0],[`license_civ_rebel`,0],[`license_civ_advrebel`,0]]"
(I already have all the database connections set up, I just need the query and PhP code to do as I mention).
I also want to make each block translate into text (e.g. license_civ_driver will be Driver License).
As well as that, I want to display each block as a div and have the background change colour based on the number within the block.
I know this is probably a huge ask but I have tried everything to my knowledge including using sites to help me do it myself (this is my attempt). If anyone can help me do it then I would be extremely grateful. If someone wants to make it for me then I would greatly appreciate that but obviously I want to learn so I'd rather be helped.
Thanks in advance!
I notice in your code that you are including many files inside a loop - some inside other loops... this is NOT the way to do it I would say. Also I believe it is something of a mistake to say "I'll sort that ( sql injection ) out later" considering "...obviously I want to learn so I'd rather be helped."
It would be better to adopt the good practises early, do it right and then not have to go back.
That said perhaps the following might be of use based upon the comments in the question rather than in-depth studying of the php.
update:
After reading your comment I added some hastily written javascript code ( and made a minor change to the php generated html ) - it's not tested mor ethan a simple click but should give the mechanism by which you can accomplish the stated goal. Use the request to trigger some PHP code ( same page or other script... ) - validate parameters/data sent, construct sql prepared statement and execute.
/* some rudimentary styles for demo purposes */
echo "
<style>
.class_low{padding:1rem;width:100%;background:rgba(255,0,0,0.25);}
.class_medium{padding:1rem;width:100%;background:rgba(0,0,255,0.25);}
.class_high{padding:1rem;width:100%;background:rgba(0,255,0,0.25);}
.unknown{padding:1rem;width:100%;background:yellow}}
</style>
<script>
const buildparams=function(p){
if( p && typeof( p )==='object' ){
p=Object.keys( p ).map(function( k ){
return typeof( p[ k ] )=='object' ? buildparams( p[ k ] ) : [ encodeURIComponent( k ), encodeURIComponent( p[ k ] ) ].join('=')
}).join('&');
}
return p;
};
const ajax=function(url,params,callback){
let xhr=new XMLHttpRequest();
xhr.onload=function(){
if( this.status==200 && this.readyState==4 )callback.call( this, this.response )
};
xhr.onerror=function(e){
alert(e)
};
xhr.open( 'POST', url, true );
xhr.setRequestHeader('Content-Type','application/x-www-form-urlencoded');
xhr.setRequestHeader('X-Requested-With','XMLHttpRequest');
xhr.send( buildparams( params ) );
};
const evtcallback=function(r){
alert(r);
};
const evtbindclicks=function(div){
div.addEventListener( 'click', evtclickhandler, false );
};
const evtclickhandler=function(e){
let url=document.location.href;
let params={ licence:this.innerHTML, value:this.dataset.value };
ajax.call( this, url, params, evtcallback );
};
document.addEventListener('DOMContentLoaded',function(){
Array.prototype.slice.call( document.querySelectorAll('.class_low, .class_medium, .class_high, .unknown') ).forEach( evtbindclicks );
},false );
</script>";
/* a lookup object to choose correct style based upon integer value */
$matrix=array(
0 => 'class_low',
1 => 'class_medium',
2 => 'class_high'
);
/* fudge around with the database column data. It would be so much easier if the data were originally json in the db! */
function makejson( $colval, $arr ){
$colval=str_replace( array( '"', '`','_' ),array('',"#",' ' ), $colval );
return json_decode( str_replace( $arr,'', str_replace( '#', '"', $colval ) ) );
}
/* words to replace in data */
$arrchrs=array('civ','license');
/* source data - ie: column data from db */
$colval="[[`license_civ_driver`,1],[`license_civ_boat`,2],[`license_civ_pilot`,15],[`license_civ_advpilot`,0],[`license_civ_trucking`,1],[`license_civ_gun`,1],[`license_civ_hunting`,0],[`license_civ_dive`,0],[`license_civ_home`,0],[`license_civ_platinum`,1],[`license_civ_oil`,1],[`license_civ_diamond`,0],[`license_civ_salt`,0],[`license_civ_sand`,0],[`license_civ_iron`,0],[`license_civ_copper`,1],[`license_civ_cement`,0],[`license_civ_rubber`,0],[`license_civ_meth`,0],[`license_civ_cocaine`,0],[`license_civ_heroin`,0],[`license_civ_marijuana`,0],[`license_civ_rebel`,0],[`license_civ_advrebel`,0]]";
/* attempt to make the above usable */
$json=makejson( $colval, $arrchrs );
/* process the data, create a new div per item and assign class using matrix */
foreach( $json as $arr ){
$class = isset( $matrix[ $arr[1] ] ) ? $matrix[ $arr[1] ] : 'unknown';
$name = $arr[0];
printf( '<div class="%s" data-value="%d">%s Licence</div>', $class, $arr[1], ucwords( $name ) );
}
This will yield something like this:

PHP table + pdfMake + AngularJS

I'm a noobie of PHP and AngularJS.
I have a webpage that communicates to a web serves with PHP - AJAX. It queries a database, and echoes the result (a big table) in an html placeholder.
I want to print the content of that table in a downloadable PDF file when the user pushes a button.
I want to use PDFmake and now it works well for test purpose, but how can I pass that content of my table to AngularJS' app?
Maybe should I pass table's id to docDefinition content? In that case I don't know how to do that.
Note: Maybe my approach is uncorrent cause I have to relegate PHP to different tasks and use AngularJS to query the Database, but for now I want to mantain this approach.
Thank You
I suggest you use an angular service (as explained in the docs
)
var bigTableApp = angular.module('bigTable',[])
bigTableApp.factory('BigTableSrv', ['$resource',
function($resource) {
return $resource('URL_to_php_backend', {}, {
query: {
method: 'GET',
params: {param1: 'value 1', param2: 'value 2'},
isArray: true
}
});
}
]);
Then, you can use it in a controller to fetch data from the back-end and build a table structure in PDFmake's table format:
bigTableApp.controller('BigTableController', ['$scope', 'BigTableSrv',
function BigTableController($scope, BigTableSrv) {
$scope.bigTable = BigTableSrv.query();
$scope.pdfMakeTable = {
// array of column widths, expand as needed
widths: [10, *, 130],
body: []
};
$scope.printTable = function() {
pdfMakeTable.body = $scope.bigTable.map(el => {
// process each element of your "big table" to one line of the
// pdfMake table, size of return array must match that of the widths array
return [el.prop1, el.prop2, el.prop3]
});
// create the pdfMake document
let docDefinition = {
content: [ pdfMakeTable ]
}
// print your pdf
pdfMake.creatPdf(docDefinition).print()
}
}
]);

Refresh cart_fragments(mini-cart) by PHP or jQuery?

Im trying to update the mini-cart (cart_fragments) after/on an ajax event. It works like this.
HTML Trigger:
<a id="woomps-button-\'.$nr.\'" class="woomps-button" '.$ajax_url.' data-button-nr="'.$nr.'">
jQuery request:
jQuery( document ).on('click', '.woomps-button', function() {
var nr= jQuery(this).data('button-nr');
jQuery.ajax({
url : subpost.ajax_url,
type : 'post',
data : {
action : 'woomps_update_nr',
security : subpost.security,
nr: nr
},
success : function( response ) {
window.alert(nr);
//MAYBE_code to refresh_fragments here
}
});
return false;
});
PHP responder:
function woomps_update_choosen_person () {
$p = $_POST['nr'];
if ($p == 1) {$x= "This must show in cart";}
WC()->session->set( 'woomps_limit_by_persons' , $x );
//MAYBE code to refresh_fragments here
}
And in the mini-cart.php template i have a calculation based on this field.
$items_left_start = WC()->session->get( 'woomps_limit_by_persons' )?: 3 ;
//Do something something here
So this works, except I need to refresh the cart like when an item is added to cart. My assumption is that is should be an ajax request from jQuery that i can put in the success block?
The class i (think) I want to fire is this WC_AJAX::get_refreshed_fragments(); But this is fired from an an add_action, so i tried this add_action( 'wp_ajax_nopriv_woocommerce_get_refreshed_fragments', array( 'WC_AJAX', 'get_refreshed_fragments' ) );. But it did not work either.
I also try to create an jQuery ajax call like it does in the add_to_cart button, but it neither worked.
//MAYBE_code to refresh_fragments here
var data = {};
jQuery.post(
wc_get_refreshed_fragments_params.wc_ajax_url.toString().
replace( '%%endpoint%%', 'get_refreshed_fragments' ), data,
function( response ){
})
}
I do not completely understand how this works, if anyone have some pointers or a snippet i would so much appriciate it. Been struggeling with this for some time now.
After much struggeling this topic on stack helped create the correct code to update mini cart fragments. It was both PHP and jQuery neeeded.
So basically you can call the WC_AJAX::get_refreshed_fragments() at the end of your PHP coode responder; if it comes from an AJAX call. It will not return to your PHP responder code so put it at the end. The PHP respons will end/sent back to jQuery inside the WC_AJAX::get_refreshed_fragments(); so you also need to create some jQuery that responds to this. This i got from the topic:
var fragments = response.fragments;
if ( fragments ) {
jQuery.each(fragments, function(key, value) {
jQuery(key).replaceWith(value);
});
}

CakePHP 2.4: Getting large, extensible forms past SecurityComponent?

What is the most secure way to submit large, extensible forms with CakePHP (2.4) and SecurityComponent?
I have a form on my app which creates new fields to store new subrecords (and sub-subrecords) using jQuery. This clashes with Cake's SecurityComponent, which expects all fields on a submitted form to have been created server-side by FormHelper.
In the past, when I've been only saving records across one association, I've been able to limit fields user-side to a high but workable number like 100, and explicitly unlock each and every possible field the form could generate:
while($i < 100){
$this->Form->unlockField('ChildModel.' . $i . '.value'); $i++;
// unlock other fields for that possible record
}
However, with this new form I have to save data across not one but two relationships. Users can potentially create a lot of sub-records or sub-sub-records, so the namespace ChildModel.[1-100].field, ChildModel.[1-100].GrandchildModel.[1-100].field starts to get huge. Unlocking a namespace of tens of thousands of possible fields, very few of which are going to be used but all of which are potentially going to be needed, starts to sound really crazy.
What solutions have other CakePHP devs found to get around this issue? I presume this is something that other people have encountered, where disabling Security for the entire action is simply not an option.
Personally I'm doing it this way:
Use AJAX to send the information of the dynamic fields to create back to the server
Genereate a form with the new inputs
Extract the token values from the generated form HTML and pass them back together with the generated HTML of the new fields
Inject the generated HTML and token values into the existing form
???
Profit!
Here's a very basic stripped example from an older project, it is used to add additional inputs for a single association.
Server side:
App::uses('Xml', 'Utility');
$formHtml = $this->Form->create('Model');
$this->Form->input('some_field');
$this->Form->input('also_a_field');
$dynamicInputs = array();
for($i = 0; $i < $numberOfEntries; $i ++)
{
$dynamicInputs[] = $this->Form->input('AssociatedModel.' . $i . '.field');
}
$formHtml .= $this->Form->end();
$xml = Xml::build($formHtml);
$formData = Xml::toArray($xml);
$data = array
(
'token' => array
(
'key' => array
(
'id' => $formData['form']['div'][0]['input'][1]['#id'],
'value' => $formData['form']['div'][0]['input'][1]['#value']
),
'fields' => array
(
'id' => $formData['form']['div'][2]['input'][0]['#id'],
'value' => $formData['form']['div'][2]['input'][0]['#value']
),
'unlocked' => array
(
'id' => $formData['form']['div'][2]['input'][1]['#id'],
'value' => $formData['form']['div'][2]['input'][1]['#value']
)
),
'dynamicInputs' => $dynamicInputs
);
echo json_encode($data);
Frontend (using jQuery):
var form = $('#my-form');
function addEntry()
{
var inputs = form.find('.associated-model .input');
var numberOfEntries = inputs.length + 1;
$.ajax({
url: '/controller/action/whatever',
type: 'POST',
data: 'numberOfEntries=' + numberOfEntries + '&' + form.serialize(),
dataType: 'json',
success: function(data)
{
updateForm(data);
}
});
}
function updateForm(data)
{
var tokenKey = form.find('input[name=\'data[_Token][key]\']');
tokenKey.attr('id', data.token.key.id);
tokenKey.attr('value', data.token.key.value);
var tokenFields = form.find('input[name=\'data[_Token][fields]\']');
tokenFields.attr('id', data.token.fields.id);
tokenFields.attr('value', data.token.fields.value);
var tokenUnlocked = form.find('input[name=\'data[_Token][unlocked]\']');
tokenUnlocked.attr('id', data.token.unlocked.id);
tokenUnlocked.attr('value', data.token.unlocked.value);
form.find('.associated-model').empty().append(data.dynamicInputs.join('\n'));
}

how to add custom columns to dataTables with server-side processing?

i have made this example work on my page http://datatables.net/examples/server_side/server_side.html, (using php5, jquery+ui and dataTables.net)
i would like to be able to add a Modify and Delete link on each row, how can i do that without sending two extra columns with the links from the server?
also how can i substitute the ids the rows have in the database and that are sent by the server with nice number starting from 1 till iTotalDisplayRecords...
thank you
found how
var controller_name = '<?php echo Zend_Controller_Front::getInstance()->getRequest()->getControllerName();?>';
"fnDrawCallback": function ( oSettings ) {
/* Need to redo the counters if filtered or sorted */
for ( var i=0, iLen=oSettings.aiDisplay.length ; i<iLen ; i++ )
{
var link = $(' Modifica Cancella');
$('td:eq(0)', oSettings.aoData[ oSettings.aiDisplay[i] ].nTr ).html( i+1 );
$('td:eq(0)', oSettings.aoData[ oSettings.aiDisplay[i] ].nTr ).append(link);
}
},

Categories