Jump to content

NotionCommotion

Members
  • Content Count

    1,886
  • Joined

  • Last visited

  • Days Won

    8

Everything posted by NotionCommotion

  1. NotionCommotion

    How to access symmetric key?

    If a client connects to a ReactPHP TLS socket server, is it possible to obtain the symmetric key from within the PHP code? Hoping it will allow me to decrypt analysis traffic between two using Wireshark.
  2. NotionCommotion

    Dependency Injection via setter method

    Who says so? A container is nice because it allows you to configure your potential objects without creating them, but I think containers are just a means to help implement DI. Or maybe I should say DI is a good means to implement composition and contains are a good means to implement DI. Just my two bits...
  3. NotionCommotion

    How to access symmetric key?

    Can you elaborate? I like it! I take it you log the entire message with either deliminator or length prefix, right? Have you ever used CBOR (which I am doing) or similar or compressed JSON? With straight JSON, it should be easy enough to determine message breaks based on visually looking for known words, but not so if scrambled. I probably need to log both the pre and post CBOR raw message, and maybe take other steps. Any lessons learned would be appreciated.
  4. NotionCommotion

    How to access symmetric key?

    Was afraid of that. Evidently, Firefox and Chrome both support logging the symmetric session key used to encrypt TLS traffic to a file, and Wireshark is configured to use this file and then can decrypted TLS traffic. See https://redflagsecurity.net/2019/03/10/decrypting-tls-wireshark/. Well, I am not accessing the connection using FF or Chrome so that doesn't help me, but maybe there is a different way to do so with some Linux command?
  5. NotionCommotion

    Get variable value to another PHP page using AJAX

    First of all, not really sure what this line is all about. echo "$voice_id, . "&". , $voice_name"; I see your "alert" test in your jQuery ajax success. That is good, but do more. Did you use Chrome's console inspector (or similar other browsers) to ensure the browser client is sending data? Next, be really simple at the server and just use exit('<pre>'.print_r($_POST).'</pre>'); or var_dump($_POST);.
  6. NotionCommotion

    Returning before complete

    Thanks kicken, When executing sleep 60, I would see a delay of 60 seconds which definitely exceeds the next loop tick. I just stumbled upon a possible culprit. At the end of the following script (which actually has timeouts, etc, but were removed in this example. if you think applicable, I can post the whole thing), you will see me performing back-to-back sends first to register and second to do the real request. $loop = \React\EventLoop\Factory::create(); (new \React\Socket\TimeoutConnector(new \React\Socket\Connector($loop), 5, $loop)) ->connect('127.0.0.1:1337') ->then(function (\React\Socket\ConnectionInterface $stream) { $lengthPrefixStream = new LengthPrefixStream($stream); $lengthPrefixStream->on('data', function($data) use ($stream){ syslog(LOG_INFO, 'on data: '.json_encode($data)); if($data['id']===1) { //response that registration was accepted } else { //real response with data } }); $lengthPrefixStream ->send($this->makeJsonRpcRequest('register',$this->logon, 1)->toArray()) ->send($jsonRpcRequest->toArray()); }); Under this scenario, I only see the on data syslog when both requests are complete. I've since changed it to first perform the registration request and only perform the real request after receiving the registration response, and now seem to get the results I wanted/expected. While I think my problem is solved, I still question why I wasn't getting the intermediate responses. Any ideas? The LengthPrefixStream class which you basically wrote is below. On a related, note, if you have any recommendations for the below class, or know why I might have previously been having it implement EventEmitterInterface, please let me know. Thanks <?php namespace NotionCommotion\SocketServer; use Evenement\EventEmitterInterface; use Evenement\EventEmitterTrait; use React\Stream\DuplexStreamInterface; /* Parses stream based on length prefix, and emits data or error if bad JSON is received. */ class LengthPrefixStream {// why => implements EventEmitterInterface { use EventEmitterTrait; private $socket=false, $debug, //If null, do not debug. If zero, don't crop message. Else log and crop. $buffer='', $encryptCBOR=null, $timestamp, $client; //Will be set after object is created public function __construct(DuplexStreamInterface $socket, int $debug=null){ $this->timestamp=time(); $this->socket = $socket; $this->socket->on('data', function($data){ $this->parseBuffer($data); }); $this->debug=$debug; $this->log('on connect with '.$socket->getRemoteAddress()); //$this->socket->on('error', function($error, $stream){$this->log($error, LOG_ERR);}); } public function setClient(AbstractClient $client):self{ $this->client=$client; return $this; } public function getClient():AbstractClient{ return $this->client; } public function getRemoteAddress():string{ return $this->socket->getRemoteAddress(); } public function getLocalAddress():string{ return $this->socket->getLocalAddress(); } public function send(array $msg, string $debug=null):self{ if($this->isConnected()) { $this->log('onSend'.($debug?" ($debug): ":': ').json_encode($msg)); $msg = $this->encode($msg); if(!$this->socket->write(pack("V", strlen($msg)).$msg)) { $this->log("LengthPrefixStream::send() buffer data: $msg", LOG_ERR); } } else { $this->log('LengthPrefixStream::send() not connected (should never happen). '.json_encode($msg), LOG_ERR); } return $this; } private function parseBuffer(string $data):void{ $this->buffer .= $data; do { $checkAgain = false; if(($bufferLength = strlen($this->buffer)) > 3 ){ $length = unpack('Vlen', substr($this->buffer, 0, 4))['len']; if(is_int($length)) { if($bufferLength >= $length + 4){ try { $msg=$this->decode(substr($this->buffer, 4, $length)); $this->log('onData: '.json_encode($msg)); $this->emit('data', [$msg]); } catch(\InvalidArgumentException $e) { $this->log('onError: '.$e->getMessage(), LOG_ERR); $this->emit('errorJson', [$e->getMessage()]); } $this->buffer = substr($this->buffer, $length+4); $checkAgain = strlen($this->buffer)>=4; } else { $this->log('Buffer is 4 or less bytes. Skip.'); } } else { $this->log('LengthPrefixStream - Invalid Prefix', LOG_ERR); $this->emit('errorPrefix', ["Invalid length prefix provided by client: ".substr($this->buffer, 0, 3)]); } } else { $this->log('Buffer less than 3 bytes. Skip.'); } } while ($checkAgain); } public function isConnected():bool { return boolval($this->socket); } public function close():void { $this->log('LengthPrefixStream::close()'); $this->socket->close(); } public function getTimestamp():int { return $this->timestamp; } private function log(string $msg, int $format=LOG_INFO):void { if(!is_null($this->debug) || $format!==LOG_INFO) { $name = $this->client?$this->client->getClientType():'No Client'; $msg=$this->debug?substr($msg, 0, $this->debug):$msg; syslog($format, "Debug ($name): $msg"); } } private function encode(array $msg):string { return $this->encryptCBOR===true?\CBOR\CBOREncoder::encode($msg):json_encode($msg); } private function decode(string $msg):array { if($this->encryptCBOR===true) { if(($rs=\CBOR\CBOREncoder::decode($msg))===false){ throw new \InvalidArgumentException('Invalid CBOR: '.substr($this->buffer, 4, $length)); } } elseif($this->encryptCBOR===false) { $rs=json_decode($msg, true); if (json_last_error() !== JSON_ERROR_NONE){ throw new \InvalidArgumentException('Invalid JSON: '.json_last_error_msg()." (".json_last_error().'): '.substr($this->buffer, 4, $length)); } } else { //First time communication $rs=json_decode($msg, true); if (json_last_error() === JSON_ERROR_NONE){ $this->encryptCBOR=false; } elseif(($rs=\CBOR\CBOREncoder::decode($msg))===false){ throw new \InvalidArgumentException('Invalid JSON: '.json_last_error_msg()." (".json_last_error().'): '.substr($this->buffer, 4, $length)); } else { $this->encryptCBOR=true; } } return $rs; } }
  7. NotionCommotion

    Returning before complete

    Browser HTTP client makes a request to a HTTP web server which makes a HTTP cURL request to a HTTP REST API which initiates a ReactPHP socket client to make a request to a socket server, and the socket server script eventually execute the following method: public function executeSpecificRequestCommand(array $data):bool { $status = $this->doSomething($data); return $status; //{success: $status} will be returned to the socket client } All is good until doSomething() takes a lot of time and results in a cURL error between the HTTP web server and HTTP REST API. For this particular case, the task isn't meant to provide immediate feedback to the user, but to do some work and update the database, and as such, my desire is to return true status and then perform the work instead of extending the cURL timeout. One option is to ran some process in the background and return status, but I don't think doing so is really right. Using a queue seems excessive as I am already decoupled via the socket. As such, I will probably just add some logic between the initial $string->on('data') and this executeSpecificRequestCommand() method to determine whether the success message should be returned before or after the method is complete. Before doing so, however, I would like to know if there is a more appropriate approach to this scenario. It appears that maybe a child process or a deferred might be appropriate, but am not sure whether I am going down the wrong path.
  8. NotionCommotion

    Returning before complete

    I am confusing promises with callbacks. But are not both promises and callbacks both design patterns for dealing with asynchronous operations, and while XMLHttpRequest might not directly provide asynchronous functionality, does it not exhibit such behavior through use of promises or callbacks? It seems to me that the main differences is if multiple callbacks are used, they must be nested and each must have its own catch(), however, promises are chained and have a single common catch(). No? My previous post showed something like the following. I would have thought that ['success'=>true] would have been first returned to the caller stream before the sleep blocking function, but I am observing differently. Do you think I have some error elsewhere and shouldn't be observing differently? function executeSpecificRequestCommand(\React\Socket\ConnectionInterface $connection){ syslog(LOG_INFO, 'start'); $msg = json_encode(['success'=>true]); $connection->write(pack("V", strlen($msg)).$msg); exec('sleep 60'); syslog(LOG_INFO, 'save results'); } $loop = \React\EventLoop\Factory::create(); (new \React\Socket\TcpServer('127.0.0.1:1337', $loop)) ->on('connection', function (\React\Socket\ConnectionInterface $connection) { $connection->on('data', executeSpecificRequestCommand($data)); }); $loop->run(); Assuming that I am observing the expected behavior, I will need to utilize either a callback or promise, or do the command in the background. Note really sure about best way to utilize callbacks, and am thinking that maybe adding a background task to a gearmanclient? For using promises, maybe something like the non-working following? function executeSpecificRequestCommand(\React\Socket\ConnectionInterface $connection, array $data){ syslog(LOG_INFO, 'start'); someAsyncOperation($data) //sleep 60 ->then(function($result){ syslog(LOG_INFO, 'save results'); }) ->catch(function($error){ // Handle error }); $msg = json_encode(['success'=>true]); $connection->write(pack("V", strlen($msg)).$msg); syslog(LOG_INFO, 'end'); } I like your use of the 200 versus 201 responses. Thanks!
  9. NotionCommotion

    Returning before complete

    Thanks requinix, Any reason this this logic shouldn't be located at the socket server instead of the HTTP server? Benefits seem to be: 1) the HTTP side shouldn't need to know it is a long process and 2) it provides more information specifically that the SS has been reached without decrease in UX. Currently, I haven't injected or passed the connection to executeSpecificRequestCommand(), but if I did, I could do something like the following: public function executeSpecificRequestCommand(array $data):void { $this->connection->send(['success'=>true]); //which will be returned to HTTP server's socket client which will then result in a HTTP 201 response $result = $this->doSomething($data); //which takes a long time $this->saveInDB($result); } But what I was trying to get at (granted, my subject title was really crappy) is "I think" XMLHttpRequest kind of does what a promise/deferred does (still got to read up). var xhttp = new XMLHttpRequest(); xhttp.onreadystatechange = function() { if (this.readyState == 4 && this.status == 200) { //response from really long process (i.e. HTTP request between client and server } }; xhttp.open("GET", "bla.php", true); xhttp.send(); ... and I shouldn't need/want to pass the connection, but instead do something like... public function executeSpecificRequestCommand(array $data):bool { $thing = new XMLHttpRequestLikePromise(); $thing->onComplete = function() { $this->saveInDB($result); }; $thing->open('doSomething', $data); return $thing->startAkaSend(); } Granted, the above script is nonsense, but hopefully communicates what I am trying to convey.
  10. NotionCommotion

    File permission issues

    I need to do a better job reading the documentation. move_uploaded_file ( string $filename , string $destination ) : bool This function checks to ensure that the file designated by filename is a valid upload file (meaning that it was uploaded via PHP's HTTP POST upload mechanism). If the file is valid, it will be moved to the filename given by destination.
  11. NotionCommotion

    File permission issues

    Maybe never mind. Just noticed ssphpd doesn't have a group... ps. do I have the umask part right? Edit 2. No, it didn't help... I have two users (ssphpd and apache) which will need to write files to a given directory and its subdirectories. My intent was to set them up with ssphd's group and use the setgid to make all future files and directories inherit that group. Not sure, but I don't think I have this right yet as I indicate at the end of this post. [michael@bb5 ~]$ sudo useradd -r ssphpd [michael@bb5 ~]$ sudo usermod -aG wheel ssphpd [michael@bb5 ~]$ sudo usermod -aG wireshark ssphpd [michael@bb5 ~]$ sudo usermod -aG ssphpd apache [michael@bb5 ~]$ groups apache apache : apache ssphpd [michael@bb5 ~]$ groups ssphpd ssphpd : ssphpd wheel wireshark [michael@bb5 ~]$ [michael@bb5 ~]$ sudo chmod g+rwx -R /var/www/~/storage [michael@bb5 ~]$ sudo chown michael.ssphpd -R /var/www/~/storage [michael@bb5 ~]$ sudo chmod g+s /var/www/~/storage My script to write this files is below: public function setFile(array $file, string $nameAlias):void { $path=$this->getPath($nameAlias, true); $this->moveTo($file['tmp_name'], $path); } private function getPath(string $name, bool $create):string { //$name is 20 characters $p1=$this->uploadDirectory.'/'.substr($name,0,2); $p2=$p1.'/'.substr($name,2,3); $p3=$p2.'/'.substr($name,5,4); $old_umask = umask(0); if($create) { if(!file_exists($p1)){mkdir($p1, 0775);} if(!file_exists($p2)){mkdir($p2, 0775);} if(!file_exists($p3)){mkdir($p3, 0775);} } umask($old_umask); return $p3.'/'.substr($name,9); } private function moveTo(string $tmpname, string $path):void { syslog(LOG_ERR, "attempting to move $tmpname to $path"); $uid = posix_getuid(); syslog(LOG_ERR, "user PHP is running as: $uid - ".posix_getpwuid($uid)['name']); $gid = posix_getgid(); syslog(LOG_ERR, "group PHP is running as: $gid - ".posix_getpwuid($gid)['name']); $this->test($tmpname); $this->test(substr($path,0,-12)); syslog(LOG_ERR, move_uploaded_file($tmpname, $path)?'success':'failure'); } private function test(string $file):void { syslog(LOG_ERR, "$file is writable: ".(is_writable($file)?'yes':'no')); $uid=fileowner($file); syslog(LOG_ERR, "$file owner: $uid - ".posix_getpwuid($uid)['name']); $gid=filegroup($file); syslog(LOG_ERR, "$file group: $gid - ".posix_getpwuid($gid)['name']); $perms=fileperms($file); syslog(LOG_ERR, "$file permissions: $perms - ".$this->formatPerms($perms)); } However, as seen, not only can only apache write files, the newly created directories are displaying apache's group instead of ssphp's group. Any ideas? Jun 06 19:40:29 bb5.net Server[8485]: attempting to move /tmp/DL_TS_6n0kmr to /var/www/dev/storage/uploads/45/699/ed6c/d15fd6959b6 Jun 06 19:40:29 bb5.net Server[8485]: user PHP is running as: 989 - ssphpd Jun 06 19:40:29 bb5.net Server[8485]: group PHP is running as: 986 - Jun 06 19:40:29 bb5.net Server[8485]: /tmp/DL_TS_6n0kmr is writable: yes Jun 06 19:40:29 bb5.net Server[8485]: /tmp/DL_TS_6n0kmr owner: 989 - ssphpd Jun 06 19:40:29 bb5.net Server[8485]: /tmp/DL_TS_6n0kmr group: 986 - Jun 06 19:40:29 bb5.net Server[8485]: /tmp/DL_TS_6n0kmr permissions: 33152 - rrw------- Jun 06 19:40:29 bb5.net Server[8485]: /var/www/dev/storage/uploads/45/699/ed6c is writable: yes Jun 06 19:40:29 bb5.net Server[8485]: /var/www/dev/storage/uploads/45/699/ed6c owner: 989 - ssphpd Jun 06 19:40:29 bb5.net Server[8485]: /var/www/dev/storage/uploads/45/699/ed6c group: 986 - Jun 06 19:40:29 bb5.net Server[8485]: /var/www/dev/storage/uploads/45/699/ed6c permissions: 16893 - drwxrwxr-x Jun 06 19:40:29 bb5.net Server[8485]: failure Jun 06 19:40:32 bb5.net Api[6981]: attempting to move /tmp/phpy1CRQR to /var/www/dev/storage/uploads/ec/911/7ab7/c73065bff0b Jun 06 19:40:32 bb5.net Api[6981]: user PHP is running as: 48 - apache Jun 06 19:40:32 bb5.net Api[6981]: group PHP is running as: 48 - apache Jun 06 19:40:32 bb5.net Api[6981]: /tmp/phpy1CRQR is writable: yes Jun 06 19:40:32 bb5.net Api[6981]: /tmp/phpy1CRQR owner: 48 - apache Jun 06 19:40:32 bb5.net Api[6981]: /tmp/phpy1CRQR group: 48 - apache Jun 06 19:40:32 bb5.net Api[6981]: /tmp/phpy1CRQR permissions: 33152 - rrw------- Jun 06 19:40:32 bb5.net Api[6981]: /var/www/dev/storage/uploads/ec/911/7ab7 is writable: yes Jun 06 19:40:32 bb5.net Api[6981]: /var/www/dev/storage/uploads/ec/911/7ab7 owner: 48 - apache Jun 06 19:40:32 bb5.net Api[6981]: /var/www/dev/storage/uploads/ec/911/7ab7 group: 48 - apache Jun 06 19:40:32 bb5.net Api[6981]: /var/www/dev/storage/uploads/ec/911/7ab7 permissions: 16893 - drwxrwxr-x Jun 06 19:40:32 bb5.net Api[6981]: success
  12. NotionCommotion

    File permission issues

    I didn't think the seggid would be applicable to files but only directories, but evidently it "kind of" does. It appears a capital S means the setgid is set but doesn't do anything. Regardless, will definitely go with find. Thanks drwxrwsr-x 3 michael michael 37 Jun 7 10:08 bbb -rw-rwSr-- 1 michael michael 0 Jun 7 10:08 x
  13. NotionCommotion

    File permission issues

    Thanks kicken, It was my understanding that the set-guid bit would apply to downstream directories, but as you indicate I was mistaken. I just made a new folder with the bit set, and then as another user added directories and see how the bit is set in those directories. But my storage directory just has the root directory set, so this will obviously not work. Any reason recursively adding the bit won't work for an existing directory? And your point about copied files preserving their original guid makes total sense now that you said it. I don't like the idea of copying files. I also would rather not use the chgrp solution. But I could create the new files in some directory other than /tmp and have the set-guid bit set on this new directory. Having apache creating uploaded files to this new directory sounds like a bad idea, but it would be simple to have ssphpd create its files there. This would require me to assign ssphpd the apache group instead of how I was originally going to do it, but doesn't seem like a big deal.
  14. NotionCommotion

    File permission issues

    Yes, I am pretty sure that both users need to be able to write to that directory. One of the users is to allow files to be uploaded via the web server. PHP is currently configured with mod_php (probably should change one of these days) so the apache user needs permission, but even if I change to php-fpm, then the php-fpm user would need permission. I then have a totally different socket server application which needs permission, and while I could run it using the apache or php-fpm user, ideally it would run as a different user. Regardless of whether I should use one or multiple users, can you see any reason why using more than one user would be an issue?
  15. NotionCommotion

    Simulating $_FILES array

    Thanks
  16. NotionCommotion

    Simulating $_FILES array

    For uploading files, I use a class which accepts an uploaded file array, validates that it has a name, tmp_name, type, error, and size key, validates that it doesn't have errors and meets various requirements, and moves it to the applicable folder. For another applicable, I will be generating the file directly on the server, however, wish to use this same class. Will I need to manually create this "file" array, or is there a native PHP function to accept a path and automatically do it? Thanks
  17. NotionCommotion

    Running PHP as a systemctl service

    I have service /usr/lib/systemd/system/socketserver.service defined as follows: [Unit] Description=Socket Server After=syslog.target [Service] ExecStart=/usr/bin/php /var/www/socket/server.php Restart=on-abort Restart=on-failure RestartSec=10s [Install] WantedBy=multi-user.target When hitting the following lines: $cmd="tshark -f 'port 1337' -i eno16780032 -a duration:4 -w /var/www/socket/tmp/test.pcap"; $status = exec($cmd, $output); syslog(LOG_INFO, 'results: '.json_encode($output).' status: '.($status?'success':'false')); I get the following: Jun 04 18:58:56 tapmeister.com php[43111]: Running as user "root" and group "root". This could be dangerous. Jun 04 18:58:56 tapmeister.com php[43111]: Capturing on 'eno16780032' Jun 04 18:58:56 tapmeister.com kernel: device eno16780032 entered promiscuous mode Jun 04 18:59:00 tapmeister.com kernel: device eno16780032 left promiscuous mode Jun 04 18:59:00 tapmeister.com php[43111]: 0 packets captured Jun 04 18:59:00 tapmeister.com Server[43111]: results: [] status: false Sure enough, exec('whoami') confirms I am running as root. Probably shouldn't be. How would you recommend configuring? PS. the three lines shown regarding tshark are executed via an asynchronous request and are not in the main loop.
  18. NotionCommotion

    Running PHP as a systemctl service

    Yes, I suppose so. In my actual service config, I had included another user, but had commented it out as I recall because it wouldn't work. Just uncommitted, and it works perfect, so don't know for sure...
  19. I have a method where the operator condition started getting too long and was becoming difficult to understand and troubleshoot. private function validateCallbackRequest():self { if($this->params->xyz < 3 || isset($this->x->bla['hi']) || soOnAndSoOnAndSoOn) { throw new Exception('Invalid params'); } return $this; } I could break it into sections, but would rather not duplicate all the exception throwing. private function validateCallbackRequest():self { if($this->params->xyz < 3 || isset($this->x->bla['hi'])) { throw new Exception('Invalid params'); } if(soOnAndSoOnAndSoOn) { throw new Exception('Invalid params'); } return $this; } I could assign variables, but don't really care for this all that much. private function validateCallbackRequest():self { $initialStuff=$this->params->xyz < 3 || isset($this->x->bla['hi']); $otherStuff=soOnAndSoOnAndSoOn; if($initialStuff || $otherStuff) { throw new Exception('Invalid params'); } return $this; } Thinking of maybe a while block and break, but not sure. Any recommendations?
  20. NotionCommotion

    Cleaner way to write long operator conditionals

    Thanks gw1500se and kicken. Appreciate the advise.
  21. NotionCommotion

    Recursively remove all array elements which are not of given type

    Why convert? Because one otherwise I need to make changes to the following and two it is a little easier to determine whether the array is associate or sequential. Real examples? Similar to the following. I wrote the class to validate a while back, and am no longer very proud of it and probably need to rewrite it one day. $rules=[ 'reconnectTimeout'=>"integer:betweenValue,5,300", 'responseTimeout'=>"integer:betweenValue,5,300", 'historyPackSize'=>"integer", 'verboseLog'=>"boolean", ]; $arrayOfRules=[[ 'deviceId'=>"integer:betweenValue,0,4194302", 'pollrate'=>"integer:betweenValue,5,600||exactValue,0", 'lifetime'=>"integer:betweenValue,0,300" ]]; <?php namespace NotionCommotion\JsonValidator; class JsonValidatorException extends \Exception { // Supported codes Configuration error: 0, Validation error: 1 private $errors, $blueprint, $input; public function __construct($message, $code=0, Exception $previous = null, $errors=false, $blueprint=false, $input=false) { $this->errors=$errors; $this->blueprint=$blueprint; $this->input=$input; parent::__construct($message, $code, $previous); } public function getError($sep=', ') { return is_array($this->errors)?implode($sep,$this->errors):$this->errors; } public function getErrorArray() { return (array) $this->errors; } public function getBlueprint() { return $this->blueprint; } public function getInput() { return $this->input; } } class JsonValidator { private $delimitor = '~'; //Internal use with object() for the replacement of parenthese content, but can be changed if conflicts with user data (not necessary?) private $strictMode=true; //Currently only enfources that boolean is true/false private $sanitize=false; //Whether to sanitize (i.e. 'false' is changed to false) public function __construct($config=[]) { if(!is_array($config)) throw JsonValidatorException('Constructor must be provided an array or no value'); if(array_diff($config,array_flip(['strictMode','delimitor','sanitize']))) throw JsonValidatorException('Invalid constructor value'); foreach($config as $key=>$value) { $this->$key=$value; } } public function object($input, $blueprint){ /* Description: Validateds JSON object's properties type and values based on a stdClass "blueprint" which specifies rules. The following describes the single public method "object()" Parameters: input. The JSON object (not associated array) to validate. blueprint. The ruleset which the JSON object must follow. Return value: $input potentially sanitized Return Errors: Will be returned via a JsonValidatorException Ruleset: Specifies the type and value of each property in the JSON object, and is also a JSON object. Types: Supported types: string, integer, double and boolean, object (stdClass only), and arrays. Note that double is used because of float since for historical reasons "double" is returned in case of a float, and not simply "float" (http://php.net/manual/en/function.gettype.php) Multiple types for a single property are not supported (except for general objects and arrays). Objects are specifying using an associated array using name/blueprint for each element. Sequential arrays are specified by a single element sequencial array which is used for all elements. Arrays and objects can be recursive. An asterisk "*" is for any type. If a string type starts with an tilde "~", it is optional. An object with any content is specified as an empty array []. An sequntial array with any content is specified by ['*'] ## REMOVE: Special types "array", "object", "object|array", and "array|object" can be specified for the general type, and if so, will not be validated recursivly and value validation methods are limited to empty and notEmpty. Values: Value rules are specified by a validation method preceeded by a colon ":". can be validated by changing the desired type from type to type:validation_method Additional variables to include with the given validation_method can be specified by including a comma "," between them. Multiple validation methods can be used and must be separated by && for AND or || for OR. Order is based on 1) parenthese, 2) AND, and 3) OR. ! is used to negate. Example: ['method'=>'string:max,5||someOtherMethod,123','params'=>['paramString'=>'(~string:ip&&minLengh,5)'],'extra'=>['name'=>'string','test'=>['string']]] */ if( !is_array($blueprint) && !(is_object($blueprint) && is_a($blueprint,'stdClass'))) throw new JsonValidatorException('Invalid blueprint provided. Must be an array or stdClass object.'); if( !is_array($input) && !(is_object($input) && is_a($input,'stdClass'))) throw new JsonValidatorException('Invalid input provided. Must be an array or stdClass object.'); $input=json_decode(json_encode($input)); $blueprint=json_decode(str_replace(' ', '', json_encode($blueprint))); //Rules cannot include accidental developer provided whitespace if(!$input && !$blueprint) return $input; $errors=$this->_object($input, $blueprint, 'base', []); if($errors) { throw new JsonValidatorException('Validation error', 1, null, $errors, $blueprint, $input); } return $input; } //Methods are protected and not private so that this class can be extended protected function _object($input, $blueprint, $level, array $errors){ //$blueprint should only be an object or an unassociated array /* $xdb_input=json_encode($input); $xdb_blueprint=json_encode($blueprint); echo("<h1>$level</h1>"); echo("<h4>input</h4>"); echo('<pre>'.print_r($input,1).'</pre>'); echo("<h4>blueprint</h4>"); echo('<pre>'.print_r($blueprint,1).'</pre>'); */ if(is_array($blueprint)) { switch(count($blueprint)) { case 0: //Can be an empty array or any object if(is_array($input)) { if(!empty($input)) { $errors[]="Unexpected sequential array provided in the '$level' object."; } } elseif(!is_object($input)) { $errors[]="Unexpected value '$input' provided in the '$level' object."; } break; case 1: if(is_array($input)) { if(is_array($blueprint[0]) || is_object($blueprint[0])) { $err=[]; foreach($input as $key=>$item) { if($e=self::_object($item, $blueprint[0], $level.'['.$key.']', $errors)) { $e[]=$err; } } if($err) { $errors[]=implode(', ',$err); } } else { //String or value (coming from sequential array) $rule=explode(':',$blueprint[0]); //[0=>typeRule,1=>validationRule] if($rule[0]!='*') { // * means any type, so skip (value validation not avaiable) foreach($input as $key=>$item) { if($this->sanitize) { $item=$this->sanitize($item, $rule[0]); } $type=gettype($item); if( $type!=$rule[0] && $this->strictBoolean($item,$rule[0])) { $errors[]="Sequential array value in the '$level' object is a $type but should be a $rule[0]."; } elseif(count($rule)==2) { $rs=self::validateValue($item, $rule[1], $level.'['.$key.']', $this->delimitor); if(!$rs[0]) { $errors[]="Invalid value in the '$level' sequential array: ".$rs[1][0]; } } } } } } elseif(is_object($input)) { $prop=implode(', ',array_keys((array)$input)); $errors[]="Unexpected property(s) '$prop' provided in the '$level' object."; } else { $errors[]="Unexpected value '$input' provided in the '$level' object."; } break; default: throw new JsonValidatorException('Sequential array blueprint may only have one element'); } } elseif(is_object($blueprint)) { if(!is_object($input)) { $prop=implode(', ',array_keys((array)$blueprint)); $errors[]="Missing property(s) '$prop' provided in the '$level' object."; if(!is_array($input)) { $errors[]="Unexpected sequential array provided in the '$level' object."; } else { $errors[]="Unexpected value '$input' provided in the '$level' object."; } } else { if($extraKeys=array_diff(array_keys((array) $input), array_keys((array) $blueprint))) { $prop=implode(', ',$extraKeys); $errors[]="Unexpected property(s) '$prop' provided in the '$level' object."; } foreach($blueprint as $prop=>$rule){ if(is_object($rule)) { if(!isset($input->$prop)) { $errors[]="Missing object '$prop' in the '$level' object."; } elseif(!is_object($input->$prop)) { $missingType=is_array($rule)?'sequential array':'value'; $errors[]="Unexpected $missingType property '$prop' in the '$level' object."; } else { $errors=array_merge($errors,self::_object($input->$prop, $blueprint->$prop, $level.'['.$prop.']', $errors)); } } elseif(is_array($rule)) { if(!isset($input->$prop)) { $errors[]="Missing sequential array '$prop' in the '$level' object."; } elseif(!is_array($input->$prop)) { $missingType=is_object($rule)?'object':'value'; $errors[]="Unexpected $missingType property '$prop' in the '$level' object."; } else { $errors=array_merge($errors,self::_object($input->$prop, $rule, $level.'['.$prop.']', $errors)); } } elseif(isset($input->$prop) || $rule[0]!='~') { //Skip if optional and not provided in input if($rule[0]=='~') { $rule=substr($rule, 1); } $rule=explode(':',$rule); //[0=>typeRule,1=>validationRule] if(!isset($input->$prop)) { $errors[]="Missing property '$prop' in the '$level' object."; } elseif($rule[0]!='*') { // * means any type, so skip (value validation not avaiable) if($this->sanitize) { $input->$prop=$this->sanitize($input->$prop, $rule[0]); } $type=gettype($input->$prop); if( $type!=$rule[0] && $this->strictBoolean($input->$prop,$rule[0])) { $errors[]="Property '$prop' in the '$level' object is a $type but should be a $rule[0]."; } elseif(count($rule)==2) { $rs=self::validateValue($input->$prop, $rule[1], $prop, $this->delimitor); if(!$rs[0]) { $errors[]="Invalid value for the '$prop' property in the '$level' object: ".$rs[1][0]; } } } //else wildcard value is considered valid. } //else optional not provided value is considered valid. } } } else { throw new JsonValidatorException('Sanity check. This should never occur.'); } return $errors; } private function sanitize($value, $type) { switch($type) { case 'string':case 'object':case 'array': //Not sanitized break; case 'integer': if(ctype_digit($value)) $value=(int)$value; break; case 'boolean': if(!is_bool($value)) $value=filter_var($value, FILTER_VALIDATE_BOOLEAN); break; case 'double': if(!is_float($value)) $value=filter_var($value, FILTER_VALIDATE_FLOAT); break; default: throw new JsonValidatorException("Invalid type '$type'"); } return $value; } private function strictBoolean($value, $rule) { //returns false only if not in strictMode and testing boolean who's value is 0 or 1 return $this->strictMode || $rule!='boolean' || !in_array($value,[0,1]); } static protected function validateValue($value, $ruleString, $prop, $delimitor) { //Store content in first tier parenthese and replace with delimitor if(strpos($ruleString, '(')) { preg_match_all('/\( ( (?: [^()]* | (?R) )* ) \)/x', $string, $match); //$match[1] holds the results $ruleString=preg_replace('/\( ( (?: [^()]* | (?R) )* ) \)/x',$delimitor,$ruleString); $i=0; //index for placeholders with parenthese } $errors=[]; foreach(explode('||',$ruleString) as $orString) { foreach(explode('&&',$orString) as $rule) { if(substr($rule,0,1)=='!') { $not=true; $rule=substr($rule,1); } else $not=false; if(substr($rule,0,1)==$delimitor) { syslog(LOG_ERR, 'JsonValidator::validateValue(): What does this do?'); $rs=self::validateValue($value,$match[1][$i++],$prop); } else { $rule=explode(',',$rule); $method=$rule[0]; unset($rule[0]); if(!method_exists(get_called_class(), $method)) throw new JsonValidatorException("Method $method does not exist."); $rs=self::$method($value,array_values($rule),$prop); } $rs[0]=$rs[0]!==$not; if(!$rs[0]) { $errors[]=$rs[1]; break; //If in an AND group, one false makes all false so no need to continue } } if($rs[0]) break; //If in an OR group, one true makes all true so no need to continue } if($rs[0])$rs[1]=[]; else { //FUTURE. Revise to return all errors with NOT state, and assemble when complete so that NOTs can be cancelled out $errors=implode(', ',$errors); $rs[1]=["Value of ".json_encode($value)." violates ".($not?"NOT($errors)":$errors)]; } return $rs; } static protected function isAssocArray($arr) { //No longer used return $arr===[]?false:array_keys($arr) !== range(0, count($arr) - 1); } // ################################################################## //Each returns whether valid (true/false), error message, and negated error message static protected function empty($v, array $a, $prop) { if(count($a)!==0) throw new JsonValidatorException('Invalid arguement count.'); return [empty((array)$v),"empty($prop)"]; } static protected function minValue($v, array $a, $prop) { if(count($a)!==1) throw new JsonValidatorException('Invalid arguement count.'); return [$v>=$a[0],"$prop>=$a[0]"]; } static protected function maxValue($v, array $a, $prop) { if(count($a)!==1) throw new JsonValidatorException('Invalid arguement count.'); return [$v<=$a[0],"$prop<=$a[0]"]; } static protected function betweenValue($v, array $a, $prop) { if(count($a)!==2) throw new JsonValidatorException('Invalid arguement count.'); sort($a); return [$v>=$a[0] && $v<=$a[1],"$a[0]>=$prop<=$a[1]"]; } static protected function exactValue($v, array $a, $prop) { if(count($a)!==1) throw new JsonValidatorException('Invalid arguement count.'); return [$v==$a[0],"$prop==$a[0]"]; } static protected function minLength($v, array $a, $prop) { if(count($a)!==1) throw new JsonValidatorException('Invalid arguement count.'); return [strlen(trim($v))>=$a[0],"strlen($prop)>=$a[0]"]; } static protected function maxLength($v, array $a, $prop) { if(count($a)!==1) throw new JsonValidatorException('Invalid arguement count.'); return [strlen(trim($v))<=$a[0],"strlen($prop)<=$a[0]"]; } static protected function betweenLength($v, array $a, $prop) { if(count($a)!==2) throw new JsonValidatorException('Invalid arguement count.'); $v=trim($v); sort($a); return [strlen($v)>=$a[0] && strlen($v)<$a[1],"$a[0]>=strlen($prop)<=$a[1]"]; } static protected function exactLength($v, array $a, $prop) { if(count($a)!==1) throw new JsonValidatorException('Invalid arguement count.'); return [strlen(trim($v))==$a[0],"strlen($prop)==$a[0]"]; } static protected function email($v, array $a, $prop) { if(count($a)!==0) throw new JsonValidatorException('Invalid arguement count.'); return [filter_var($v, FILTER_VALIDATE_EMAIL),"valid_email($prop)"]; } static protected function url($v, array $a, $prop) { if(count($a)!==0) throw new JsonValidatorException('Invalid arguement count.'); return [filter_var($v, FILTER_VALIDATE_URL),"valid_url($prop)"]; } static protected function ipaddress($v, array $a, $prop) { if(count($a)!==0) throw new JsonValidatorException('Invalid arguement count.'); return [filter_var($v, FILTER_VALIDATE_IP),"valid_ip($prop)"]; } }
  22. Can anyone let me know what I am doing wrong. I am sure it will (after the fact) be obvious, but I don't see it right now. Wish to remove all array elements which do not implement ValidatorCallbackInterface. Thanks <?php interface ValidatorCallbackInterface{} class ValidatorCallback implements ValidatorCallbackInterface{} function array_filter_recursive($input) { foreach ($input as &$value) { if (is_array($value)) { $value = array_filter_recursive($value); } } return array_filter($input, function($v) { return $v instanceOf ValidatorCallbackInterface; }); } function recursive_unset(&$array) { foreach ($array as $key => $value) { if (is_array($value)) { recursive_unset($value); if(empty($value)) { unset($array[$key]); } } elseif(!$value instanceOf ValidatorCallbackInterface) { unset($array[$key]); } } } $validatorCallback = new ValidatorCallback(); $rules=[ 'callbackId'=>"integer", 'info'=>[ 'arrayofobjects'=>[$validatorCallback], 'foo1'=>'bar1' ], 'foo2'=>'bar2', 'bla'=>[ 'a'=>'aa', 'b'=>'bb', ], 'singleobject'=>$validatorCallback ]; echo('original rules'.PHP_EOL); var_dump($rules); $desiredrules=[ 'info'=>[ 'arrayofobjects'=>[$validatorCallback] ], 'singleobject'=>$validatorCallback ]; echo('desired rules'.PHP_EOL); var_dump($desiredrules); echo('array_filter_recursive'.PHP_EOL); var_dump(array_filter_recursive($rules)); echo('recursive_unset'.PHP_EOL); recursive_unset($rules); var_dump($rules); original rules array(5) { ["callbackId"]=> string(7) "integer" ["info"]=> array(2) { ["arrayofobjects"]=> array(1) { [0]=> object(ValidatorCallback)#1 (0) { } } ["foo1"]=> string(4) "bar1" } ["foo2"]=> string(4) "bar2" ["bla"]=> array(2) { ["a"]=> string(2) "aa" ["b"]=> string(2) "bb" } ["singleobject"]=> object(ValidatorCallback)#1 (0) { } } desired rules array(2) { ["info"]=> array(1) { ["arrayofobjects"]=> array(1) { [0]=> object(ValidatorCallback)#1 (0) { } } } ["singleobject"]=> object(ValidatorCallback)#1 (0) { } } array_filter_recursive array(1) { ["singleobject"]=> object(ValidatorCallback)#1 (0) { } } recursive_unset array(2) { ["info"]=> array(2) { ["arrayofobjects"]=> array(1) { [0]=> object(ValidatorCallback)#1 (0) { } } ["foo1"]=> string(4) "bar1" } ["singleobject"]=> object(ValidatorCallback)#1 (0) { } }
  23. NotionCommotion

    Recursively remove all array elements which are not of given type

    The validation already works. My potential solution posted in my previous post however does not. If an array is something like ['integer'], that means the JSON must have a sequential array of integers. Doing the json_encode/decode trick takes care of this, but my previous attempt converts array ['integer'] to stdClass bla->0='integer'. Think array_walk_recursive be the best fit?
  24. NotionCommotion

    Recursively remove all array elements which are not of given type

    Maybe... function array2Obj(array $rules, ?\stdClass $obj=null):\stdClass { $obj=$obj??new \stdClass; foreach($rules as $key => $rule) { if (is_array($rule)) { build($rule, $obj); } elseif(!$rule instanceOf ValidatorCallbackInterface) { $obj->$key=str_replace(' ', '', $rule); } else { $obj->$key=$rule; } } return $obj; }
  25. NotionCommotion

    Recursively remove all array elements which are not of given type

    Agree, but I thought the custom array_filter_recursive() function would make it so. This was only an interim step, and I am now thinking probably not a good way to go. May actual end goal is convert an array to a stdClass and remove all whitespace from all the values. Actually, my real end goal is to validate JSON given a set of rules defined in an array. I was previously doing so using the following: $rules=json_decode(str_replace(' ', '', json_encode($rules))); But now, I wish to allow more than just stdClass objects. Any other recommendations how to accomplish? Thanks
×

Important Information

We have placed cookies on your device to help make this website better. You can adjust your cookie settings, otherwise we'll assume you're okay to continue.