fileHelper = $this->Application->recallObject('FileHelper'); // 5 minutes execution time @set_time_limit(5 * 60); } /** * Handles the upload. * * @param kEvent $event Event. * * @return string * @throws kUploaderException When upload could not be handled properly. */ public function handle(kEvent $event) { $this->disableBrowserCache(); // Uncomment this one to fake upload time // sleep(5); if ( !$this->Application->HttpQuery->Post ) { // Variables {field, id, flashsid} are always submitted through POST! // When file size is larger, then "upload_max_filesize" (in php.ini), // then these variables also are not submitted. throw new kUploaderException('File size exceeds allowed limit.', 413); } if ( !$this->checkPermissions($event) ) { // 403 Forbidden throw new kUploaderException('You don\'t have permissions to upload.', 403); } $value = $this->Application->GetVar('file'); if ( !$value || ($value['error'] != UPLOAD_ERR_OK) ) { // 413 Request Entity Too Large (file uploads disabled OR uploaded file was // too large for web server to accept, see "upload_max_filesize" in php.ini) throw new kUploaderException('File size exceeds allowed limit.', 413); } $value = $this->Application->unescapeRequestVariable($value); $tmp_path = WRITEABLE . '/tmp/'; $filename = $this->getUploadedFilename() . '.tmp'; $id = $this->Application->GetVar('id'); if ( $id ) { $filename = $id . '_' . $filename; } if ( !is_writable($tmp_path) ) { // 500 Internal Server Error // check both temp and live upload directory throw new kUploaderException('Write permissions not set on the server, please contact server administrator.', 500); } $filename = $this->fileHelper->ensureUniqueFilename($tmp_path, $filename); $field_options = $this->getFieldOptions($this->Application->GetVar('field'), $event); $storage_format = isset($field_options['storage_format']) ? $field_options['storage_format'] : false; $file_path = $tmp_path . $filename; $actual_file_path = $this->moveUploadedFile($file_path); if ( $storage_format && $file_path == $actual_file_path ) { $this->resizeUploadedFile($file_path, $storage_format); } if ( getArrayValue($field_options, 'file_types') && !$this->fileHelper->extensionMatch(kUtil::removeTempExtension($filename), $field_options['file_types']) ) { throw new kUploaderException('File is not an allowed file type.', 415); } if ( filesize($actual_file_path) > $field_options['max_size'] ) { throw new kUploaderException('File size exceeds allowed limit.', 413); } $this->deleteTempFiles($tmp_path); $thumbs_path = preg_replace('/^' . preg_quote(FULL_PATH, '/') . '/', '', $tmp_path, 1); $thumbs_path = FULL_PATH . THUMBS_PATH . $thumbs_path; if ( file_exists($thumbs_path) ) { $this->deleteTempFiles($thumbs_path); } return preg_replace('/^' . preg_quote($id, '/') . '_/', '', basename($file_path)); } /** * Resizes uploaded file. * * @param string $file_path File path. * @param string $format Format. * * @return boolean */ public function resizeUploadedFile(&$file_path, $format) { /** @var ImageHelper $image_helper */ $image_helper = $this->Application->recallObject('ImageHelper'); // Add extension, so that "ImageHelper::ResizeImage" can work. $resize_file_path = tempnam(WRITEABLE . '/tmp', 'uploaded_') . '.jpg'; if ( rename($file_path, $resize_file_path) === false ) { return false; } $resized_file_path = $this->fileHelper->urlToPath( $image_helper->ResizeImage($resize_file_path, $format) ); $file_path = $this->replaceFileExtension( $file_path, pathinfo($resized_file_path, PATHINFO_EXTENSION) ); return rename($resized_file_path, $file_path); } /** * Replace extension of uploaded file. * * @param string $file_path File path. * @param string $new_file_extension New file extension. * * @return string */ protected function replaceFileExtension($file_path, $new_file_extension) { $file_path_without_temp_file_extension = kUtil::removeTempExtension($file_path); $current_file_extension = pathinfo($file_path_without_temp_file_extension, PATHINFO_EXTENSION); // Format of resized file wasn't changed. if ( $current_file_extension === $new_file_extension ) { return $file_path; } $ret = preg_replace( '/\.' . preg_quote($current_file_extension, '/') . '$/', '.' . $new_file_extension, $file_path_without_temp_file_extension ); // Add ".tmp" later, since it was removed. if ( $file_path_without_temp_file_extension !== $file_path ) { $ret .= '.tmp'; } // After file extension change resulting filename might not be unique in that folder anymore. $path = pathinfo($ret, PATHINFO_DIRNAME); return $path . '/' . $this->fileHelper->ensureUniqueFilename($path, basename($ret)); } /** * Sends headers to ensure, that response is never cached. * * @return void */ protected function disableBrowserCache() { header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); header('Cache-Control: no-store, no-cache, must-revalidate'); header('Cache-Control: post-check=0, pre-check=0', false); header('Pragma: no-cache'); } /** * Checks, that flash uploader is allowed to perform upload * * @param kEvent $event * @return bool */ protected function checkPermissions(kEvent $event) { // Flash uploader does NOT send correct cookies, so we need to make our own check $cookie_name = 'adm_' . $this->Application->ConfigValue('SessionCookieName'); $this->Application->HttpQuery->Cookie['cookies_on'] = 1; $this->Application->HttpQuery->Cookie[$cookie_name] = $this->Application->GetVar('flashsid'); // this prevents session from auto-expiring when KeepSessionOnBrowserClose & FireFox is used $this->Application->HttpQuery->Cookie[$cookie_name . '_live'] = $this->Application->GetVar('flashsid'); /** @var Session $admin_session */ $admin_session = $this->Application->recallObject('Session.admin'); if ( $this->Application->permissionCheckingDisabled($admin_session->RecallVar('user_id')) ) { return true; } // copy some data from given session to current session $backup_user_id = $this->Application->RecallVar('user_id'); $this->Application->StoreVar('user_id', $admin_session->RecallVar('user_id')); $backup_user_groups = $this->Application->RecallVar('UserGroups'); $this->Application->StoreVar('UserGroups', $admin_session->RecallVar('UserGroups')); // check permissions using event, that have "add|edit" rule $check_event = new kEvent($event->getPrefixSpecial() . ':OnProcessSelected'); $check_event->setEventParam('top_prefix', $this->Application->GetTopmostPrefix($event->Prefix, true)); /** @var kEventHandler $event_handler */ $event_handler = $this->Application->recallObject($event->Prefix . '_EventHandler'); $allowed_to_upload = $event_handler->CheckPermission($check_event); // restore changed data, so nothing gets saved to database $this->Application->StoreVar('user_id', $backup_user_id); $this->Application->StoreVar('UserGroups', $backup_user_groups); return $allowed_to_upload; } /** * Returns uploaded filename. * * @return string */ protected function getUploadedFilename() { if ( isset($_REQUEST['name']) ) { $file_name = $_REQUEST['name']; } elseif ( !empty($_FILES) ) { $file_name = $_FILES['file']['name']; } else { $file_name = uniqid('file_'); } return $file_name; } /** * Returns field options. * * @param string $field Field. * @param kEvent $event Event. * * @return array */ protected function getFieldOptions($field, kEvent $event) { /** @var array $fields */ $fields = $this->Application->getUnitOption($event->Prefix, 'Fields'); /** @var array $virtual_fields */ $virtual_fields = $this->Application->getUnitOption($event->Prefix, 'VirtualFields'); return array_key_exists($field, $fields) ? $fields[$field] : $virtual_fields[$field]; } /** * Moves uploaded file to given location. * * @param string $file_path File path. * * @return string * @throws kUploaderException When upload could not be handled properly. */ protected function moveUploadedFile($file_path) { // Chunking might be enabled. $chunk = (int)$this->Application->GetVar('chunk', 0); $chunks = (int)$this->Application->GetVar('chunks', 0); $actual_file_path = $file_path . '.part'; // Open temp file. if ( !$out = @fopen($actual_file_path, $chunks ? 'ab' : 'wb') ) { throw new kUploaderException('Failed to open output stream.', 102); } if ( !empty($_FILES) ) { if ( $_FILES['file']['error'] || !is_uploaded_file($_FILES['file']['tmp_name']) ) { throw new kUploaderException('Failed to move uploaded file.', 103); } // Read binary input stream and append it to temp file. if ( !$in = @fopen($_FILES['file']['tmp_name'], 'rb') ) { throw new kUploaderException('Failed to open input stream.', 101); } } else { if ( !$in = @fopen('php://input', 'rb') ) { throw new kUploaderException('Failed to open input stream.', 101); } } while ( $buff = fread($in, 4096) ) { fwrite($out, $buff); } @fclose($out); @fclose($in); // Check if file has been uploaded. if ( !$chunks || $chunk == $chunks - 1 ) { // Strip the temp .part suffix off. rename($actual_file_path, $file_path); $actual_file_path = $file_path; } return $actual_file_path; } /** * Delete temporary files, that won't be used for sure * * @param string $path * @return void */ protected function deleteTempFiles($path) { $files = glob($path . '*.*'); $max_file_date = strtotime('-1 day'); foreach ( $files as $file ) { if ( filemtime($file) < $max_file_date ) { unlink($file); } } } /** * Prepares object for operations with file on given field. * * @param kEvent $event Event. * @param string $field Field. * * @return kDBItem */ public function prepareUploadedFile(kEvent $event, $field) { /** @var kDBItem $object */ $object = $event->getObject(Array ('skip_autoload' => true)); $filename = $this->getSafeFilename(); if ( !$filename ) { $object->SetDBField($field, ''); return $object; } // set current uploaded file if ( $this->Application->GetVar('tmp') ) { $options = $object->GetFieldOptions($field); $options['upload_dir'] = WRITEBALE_BASE . '/tmp/'; unset($options['include_path']); $object->SetFieldOptions($field, $options); $filename = $this->Application->GetVar('id') . '_' . $filename; } $object->SetDBField($field, $filename); return $object; } /** * Returns safe version of filename specified in url * * @return bool|string * @access protected */ protected function getSafeFilename() { $filename = $this->Application->GetVar('file'); $filename = $this->Application->unescapeRequestVariable($filename); if ( (strpos($filename, '../') !== false) || (trim($filename) !== $filename) ) { // when relative paths or special chars are found template names from url, then it's hacking attempt return false; } return $filename; } } class kUploaderException extends Exception { }