Invision Community 4: SEO, prepare for v5 and dormant account notifications By Matt Monday at 02:04 PM
Brew Digital Posted August 16, 2022 Posted August 16, 2022 Hi all, I've got an application that has an upload field that is giving me the "UPLOAD_FIELD_NOT_OBJECT" error, it's an app that we have had for several years and has worked on older versions of Invision. I understand why it's giving me the error and everything, but the attachments that I am using are actually being put in the uploads folder, it's just failing at the point of inserting the data into the custom database. Traditionally we have always just stored a reference to the file and it's folder and that has been fine. My main question on it is can I carry on with just storing the file reference rather than storing the upload object? So the code I have currently gets all of the attached files from the PHP $_FILES global and then sorts through the attachments and constructs strings to be inserted on the field in the database like this foreach ($attachments as $fieldID => $files) { foreach ($files as $file) { \IPS\File::validateUpload($file, $allowedFileTypes, $maxFileSize); $file = \IPS\File::create('core_Attachment', $file['name'], null, null, false, $file['tmp_name'], true); if ($file instanceof \IPS\File) { if (array_key_exists($fieldID, $customFields)) { $customFields[$fieldID] .= ',' . $file->container . '/' . $file->filename; } else { $customFields[$fieldID] = $file->container . '/' . $file->filename; } } } } I've tried doing things like: $customFields[$fieldID][$file->filename] = (string) $file; as I've seen people talking about using the file object in that sort of way. If all of that looks completely wrong let me know, any help or just thoughts as to another approach would greatly appreciated
teraßyte Posted August 17, 2022 Posted August 17, 2022 With just that code it's hard to say anything really. It would also help if you post the full backtrace of the error rather than just "UPLOAD_FIELD_NOT_OBJECT".
Brew Digital Posted August 17, 2022 Author Posted August 17, 2022 Yep, sorry I should have been clearer with that. A little context, the custom app posts various fields of information and has a standalone 'uploads' field that is causing the problem here, when it's left blank all is good. Anyway the error code I'm getting back currently is this: { "status": false, "message": "Submission failed", "data": { "error": { "errorCode": "1S306/E", "errorMessage": "UPLOAD_FIELD_NOT_OBJECT" } } } Not sure where else I can go to get anything better, can't see anything in the logs from when I've just replicated that error on my local setup. I'm reasonably new to the Invision development world (not to dev) so still getting familiar with the code that I have inherited. If you know of anything else I can provide to possibly help debug the situation that would be really helpful.
Stuart Silvester Posted August 17, 2022 Posted August 17, 2022 Can you share more about how your code works? The error message is coming from the Pages REST API. If this is in an Invision Community Application, I'm slightly confused why you would be trying to use the REST API?
teraßyte Posted August 17, 2022 Posted August 17, 2022 (edited) Yeah, I thought it was a custom app at first but that error comes from the CMS API to create/update a record. Specifically the code that handles files for a custom upload field: elseif ( $field->type === 'Upload' ) { $multiple = $field->is_multiple; $imageOnly = ( isset( $field->extra[ 'type' ] ) AND $field->extra[ 'type' ] === 'image' ); $extensions = \is_array( $field->allowed_extensions ) ? $field->allowed_extensions : array(); /* Did they meet the api parameter type requirement (the field must be an object) */ if ( !\is_array( \IPS\Request::i()->fields[ $field->id ] ) ) { throw new \IPS\Api\Exception( 'UPLOAD_FIELD_NOT_OBJECT', '1S306/E', 400 ); } It it was working before I guess the API code is old while the forum was updated at some point and the API code changed but the external code was not updated. Edited August 17, 2022 by teraßyte
Brew Digital Posted August 17, 2022 Author Posted August 17, 2022 I can try and share a little more about the code, but my knowledge is limited by the fact that I have inherited this and didn't originally build it, so if it has been gone about the wrong way, please do let me know. The app itself was requested by the client to be a more bespoke way of handling the whole process of creating a topic, they have a lot of custom category and tag information that wanted to display in a very specific way. So one of my predecessors has written the application as a multi-step form with various methods for managing the interactions back with the custom database instance, done via the REST API as you have discovered, 99% of it works perfectly fine, just having issues with this handling of the standalone 'attachments' field (yes I know they could be using the attachments in the content fields as well). So the method that handles the attachments did look like this: function _handleFormUploads($customFields, $maxFileSize, $allowedFileTypes) { $attachments = []; foreach ($_FILES as $key => $file) { if (\mb_strpos($key, 'field_') !== false) { // First ensure file key is named as expected before handling it if (!empty($file['name']) && \is_array($file['name'])) { // Multiple file upload $file = _parseMultiUpload($file); foreach ($file as $f) { $attachments[_removePrefix($key, 'field_')][] = $f; } } else { // Single file upload $attachments[_removePrefix($key, 'field_')][] = $file; } } } foreach ($attachments as $fieldID => $files) { foreach ($files as $file) { \IPS\File::validateUpload($file, $allowedFileTypes, $maxFileSize); $file = \IPS\File::create('core_Attachment', $file['name'], null, null, false, $file['tmp_name'], true); if ($file instanceof \IPS\File) { // Store the resultant file path via the API request if (array_key_exists($fieldID, $customFields)) { $customFields[$fieldID] .= ',' . $file->container . '/' . $file->filename; } else { $customFields[$fieldID] = $file->container . '/' . $file->filename; } } } } return $customFields; } I've tried hacking around that as mentioned above to get the upload data to save into the db but with no joy. As I say if this is definitely NOT the way to go about doing this sort of thing then do tell me, but if there is a way that it can be made to work I would be very interested. Like I say I am still learning Invision, so apologies if I am making terrible schoolboy errors.
Brew Digital Posted August 17, 2022 Author Posted August 17, 2022 1 hour ago, teraßyte said: Yeah, I thought it was a custom app at first but that error comes from the CMS API to create/update a record. Specifically the code that handles files for a custom upload field: elseif ( $field->type === 'Upload' ) { $multiple = $field->is_multiple; $imageOnly = ( isset( $field->extra[ 'type' ] ) AND $field->extra[ 'type' ] === 'image' ); $extensions = \is_array( $field->allowed_extensions ) ? $field->allowed_extensions : array(); /* Did they meet the api parameter type requirement (the field must be an object) */ if ( !\is_array( \IPS\Request::i()->fields[ $field->id ] ) ) { throw new \IPS\Api\Exception( 'UPLOAD_FIELD_NOT_OBJECT', '1S306/E', 400 ); } It it was working before I guess the API code is old while the forum was updated at some point and the API code changed but the external code was not updated. Yes it worked on an older version of Invision and as I'm trying to update it, it's why I am now running into problems.
teraßyte Posted August 18, 2022 Posted August 18, 2022 Your best option would be to compare the API file to see how the code actually changed. From which version did you upgrade? The code above doesn't help though. We'd need to see the code that actually calls the API to see in which format the data is being sent. That's where the error is.
Brew Digital Posted August 18, 2022 Author Posted August 18, 2022 We have upgraded from version 4.5.4.2 at that point all of this functionality still worked. So the API connection is setup through this class (slightly amended to remove some comments etc.) <?php namespace IPS\pslshare; class InvisionAPI { private $db; private $apiUrl; private $apiKey; private $curl; private $endpointUrl; public function __construct($apiUrl, $apiKey) { $this->db = \IPS\Db::i(); $this->apiUrl = $apiUrl; $this->apiKey = $apiKey; $this->curl = null; $this->endpointUrl = ''; } public function __destruct() { $this->closeCurl(); } public function getEndpointUrl() { return $this->endpointUrl; } /** * @param $endpoint * @param string $params * @throws \Exception */ private function initCurl($endpoint, $params = '') { $queryString = (!empty($params)) ? '?' . $params : ''; $this->endpointUrl = $this->apiUrl . $endpoint . $queryString; $this->curl = curl_init($this->endpointUrl); if (!$this->curl) { throw new \Exception('Failed to initialise cURL'); } curl_setopt_array( $this->curl, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPAUTH => CURLAUTH_BASIC, CURLOPT_USERPWD => "{$this->apiKey}:" ] ); } private function convertArrayToString(array $array) { return http_build_query($array); } private function setupPost(array $fields) { curl_setopt_array( $this->curl, [ CURLOPT_POST => count($fields), CURLOPT_POSTFIELDS => $this->convertArrayToString($fields) ] ); } private function closeCurl() { if (isset($this->curl)) { curl_close($this->curl); } } /** * @return bool|string * @throws \Exception */ private function processRequest() { $response = curl_exec($this->curl); if ($response === false) { throw new \Exception(curl_error($this->curl), curl_errno($this->curl)); } return $response; } private function requestPost($endpoint, $fields) { try { $this->initCurl($endpoint); $this->setupPost($fields); return $this->processRequest(); } catch (\Exception $e) { trigger_error(sprintf('Curl failed with error #%d: %s', $e->getCode(), $e->getMessage()), E_USER_ERROR); } } private function generateRandomString($length) { $keyspace = '0123456789bcdfghjklmnpqrstvwxyzBCDFGHJKLMNPQRSTVWXYZ'; $str = ''; $max = \mb_strlen($keyspace, '8bit') - 1; for ($i = 0; $i < $length; ++$i) { $str .= $keyspace[\random_int(0, $max)]; } return $str; } private function generateUniqueReference($databaseID, $length = 15) { do { $ref = $this->generateRandomString($length); $count = $this->db->select( 'COUNT(*) as count', 'cms_custom_database_' . $databaseID, ['field_33 = ?', $ref] )->first(); } while ($count >= 1); return $ref; } public function createDatabasePage($databaseID, $categoryID, $authorID, $fields, $tags, $hidden = 1, $anonymous) { $fields[33] = $this->generateUniqueReference($databaseID); if (empty($fields[2])) { $fields[2] = ' '; } if (!empty($fields[7])) { $fields[7] = \strtotime($fields[7]); } if (!empty($fields[29])) { $fields[29] = \strtotime($fields[29]); } if (!empty($fields[30])) { $fields[30] = \strtotime($fields[30]); } if (!empty($fields[31])) { $fields[31] = \strtotime($fields[31]); } if (!empty($fields[36])) { $fields[36] = \strtotime($fields[36]); } $anonymousBool = $anonymous; $fields = [ 'category' => $categoryID, 'author' => $authorID, 'fields' => $fields, 'tags' => $tags, 'hidden' => $hidden, 'ip_address' => $_SERVER['REMOTE_ADDR'], 'anonymous' => $anonymousBool ]; return $this->requestPost('cms/records/' . $databaseID, $fields); } } The $anonymousBool in there is the only recent addition to obviously give the ability for the post to be anonymous, everything else is legacy. The class is called through this (also removed a chunk of this for legibility and brevity sake, anything outside of this that you think would be useful let me know. protected function post() { $member = \IPS\Member::loggedIn(); $api = new InvisionAPI($this->baseURL . '/api/', $apiKey); $submission = _handleFormFields(); $response = $api->createDatabasePage( $databaseID, $submission['category'], $authorID, $submission['fields'], $submission['tags'], $requiresModerating, $submission['anonymous'], ); exit(); } function _handleFormFields() { $category = null; $tags = null; $authorVisibility = null; $customFields = []; foreach ($_POST as $key => $value) { switch ($key) { case 'article-type' : $customFields[3] = $value; break; case 'content-type' : $customFields[4] = $value; break; case 'category' : $category = \intval($value); break; case 'tags' : $tags = $value; break; case 'author-visibility' : $authorVisibility = $value === 'anonymous' ? 1 : 0; break; default : if (\mb_strpos($key, 'field_') !== false) { if (\is_array($value)) { $value = \implode(',', $value); } $customFields[_removePrefix($key, 'field_')] = $value; } } } return [ 'category' => $category, 'tags' => $tags, 'author-visibility' => $authorVisibility, 'anonymous' => $authorVisibility, 'fields' => $customFields ]; } Massive thanks for any assistance given on this front.
Recommended Posts