diff --git a/attachments_component/admin/config.xml b/attachments_component/admin/config.xml index 6156f067..f99fd02d 100644 --- a/attachments_component/admin/config.xml +++ b/attachments_component/admin/config.xml @@ -201,7 +201,7 @@
+ + + + diff --git a/attachments_component/admin/forms/attachment.xml b/attachments_component/admin/forms/attachment.xml index 02e98c1d..3ded9480 100644 --- a/attachments_component/admin/forms/attachment.xml +++ b/attachments_component/admin/forms/attachment.xml @@ -6,7 +6,9 @@ readonly="true" class="readonly" /> + label="JPUBLISHED" description="" + layout="joomla.form.field.radio.switcher" + > @@ -24,6 +26,7 @@ /> - + + + + + @@ -106,5 +124,65 @@ readonly="true" class="readonly" size="10" /> + + + + + + + + + +
+
+ + + +
+ diff --git a/attachments_component/admin/language/en-GB/en-GB.com_attachments.ini b/attachments_component/admin/language/en-GB/en-GB.com_attachments.ini index 95f166f8..9883e419 100644 --- a/attachments_component/admin/language/en-GB/en-GB.com_attachments.ini +++ b/attachments_component/admin/language/en-GB/en-GB.com_attachments.ini @@ -414,4 +414,17 @@ ATTACH_FILE_SIZE_DESC="File Size descending" ATTACH_MODIFIED_ASC="Modified ascending" ATTACH_MODIFIED_DESC="Modified descending" ATTACH_USER_FIELD_N_ASC="%s ascending" -ATTACH_USER_FIELD_N_DESC="%s descending" \ No newline at end of file +ATTACH_USER_FIELD_N_DESC="%s descending" + +ATTACH_TEST_IS_SECURE_FILE="Test uploaded file security" +ATTACH_TEST_IS_SECURE_FILE_DESCRIPTION="Test if uploaded file is secure: prevent files with a null byte in their name, do not allow forbiden extensions anywhere in the file's extension, Do not allow `< ? php` or `< ?` tag in content, etc., if set to No - allow all files to be uploaded without checking." + +ATTACH_COM_CONTENT_SELECT_ARTICLE="Select an article"; +ATTACH_COM_CONTENT_SELECT_ARTICLE_DESCRIPTION="Select an article to which we want Attachment to be attached."; +ATTACH_COM_CONTENT_SELECT_CATEGORY="Select a category"; +ATTACH_COM_CONTENT_SELECT_CATEGORY_DESCRIPTION="Select a category to which we want Attachment to be attached."; +ATTACH_COM_CONTENT_ARTICLE="Article" +ATTACH_COM_CONTENT_CATEGORY="Category" + +ATTACH_ERROR_INVALID_PARENT_ENTITY_NOT_SELECTED="Entity for parent ID (%d) not selected." +ATTACH_FILENAME_CURRENT="Current file name: %s" \ No newline at end of file diff --git a/attachments_component/admin/src/Controller/AttachmentController.php b/attachments_component/admin/src/Controller/AttachmentController.php index 94b8ec3d..45c209f3 100644 --- a/attachments_component/admin/src/Controller/AttachmentController.php +++ b/attachments_component/admin/src/Controller/AttachmentController.php @@ -90,8 +90,9 @@ public function add() { // Fail gracefully if the Attachments plugin framework plugin is disabled if (!PluginHelper::isEnabled('attachments', 'framework')) { - echo '

' . Text::_('ATTACH_WARNING_ATTACHMENTS_PLUGIN_FRAMEWORK_DISABLED') . '

'; - return; + $this->app->enqueueMessage(Text::_('ATTACH_WARNING_ATTACHMENTS_PLUGIN_FRAMEWORK_DISABLED')); + + return false; } // Access check. @@ -110,407 +111,136 @@ public function add() return false; } - $parent_entity = 'default'; - - // Get the parent info - $input = $app->getInput(); - if ($input->getString('article_id')) { - $pidarr = explode(',', $input->getString('article_id')); - $parent_type = 'com_content'; - } else { - $pidarr = explode(',', $input->getString('parent_id', '')); - $parent_type = $input->getCmd('parent_type', 'com_content'); - - // If the entity is embedded in the parent type, split them - if (strpos($parent_type, '.')) { - $parts = explode('.', $parent_type); - $parent_type = $parts[0]; - $parent_entity = $parts[1]; - } - } + $parentEntity = 'default'; + $parentType = $this->input->getString('parent_type', ''); - // Special handling for categories - if ($parent_type == 'com_categories') { - $parent_type = 'com_content'; + if (strpos($parentType, '.')) { + $parentInfo = explode('.', $parentType); + $parentType = $parentInfo[0]; + $parentEntity = $parentInfo[1]; } - // Get the parent id and see if the parent is new - $parent_id = null; - $new_parent = false; - if (is_numeric($pidarr[0])) { - $parent_id = (int)$pidarr[0]; - } - if ((count($pidarr) == 1) && ($pidarr[0] == '')) { - // Called from the [New] button - $parent_id = null; - } - if (count($pidarr) > 1) { - if ($pidarr[1] == 'new') { - $new_parent = true; - } - } - - // Set up the "select parent" button - PluginHelper::importPlugin('attachments'); - $apm = AttachmentsPluginManager::getAttachmentsPluginManager(); - $entity_info = $apm->getInstalledEntityInfo(); - $parent = $apm->getAttachmentsPlugin($parent_type); - - $parent_entity = $parent->getCanonicalEntityId($parent_entity); - $parent_entity_name = Text::_('ATTACH_' . $parent_entity); - - if (!$parent_id) { - // Set up the necessary javascript - AttachmentsJavascript::setupJavascript(); - - $document = $app->getDocument(); - $js = ' - function jSelectParentArticle(id, title, catid, object) { - document.getElementById("parent_id").value = id; - document.getElementById("parent_title").value = title; - let modal = bootstrap.Modal.getInstance(document.getElementById("modal-attachment")); - modal.hide(); - }'; - $document->addScriptDeclaration($js); - } else { - if (!is_numeric($parent_id)) { - $errmsg = Text::sprintf('ATTACH_ERROR_INVALID_PARENT_ID_S', $parent_id) . ' (ERR 122)'; - throw new \Exception($errmsg, 500); - } + if (($parentType == '') || ($parentType == 'com_categories')) { + $parentType = 'com_content'; } // Use a component template for the iframe view (from the article editor) - $from = $input->getWord('from'); + $from = $this->input->getWord('from', ''); if ($from == 'closeme') { - $input->set('tmpl', 'component'); - } - - // Disable the main menu items - $input->set('hidemainmenu', 1); - - // Get the article title - $parent_title = false; - if (!$new_parent) { - PluginHelper::importPlugin('attachments'); - $apm = AttachmentsPluginManager::getAttachmentsPluginManager(); - if (!$apm->attachmentsPluginInstalled($parent_type)) { - // Exit if there is no Attachments plugin to handle this parent_type - $errmsg = Text::sprintf('ATTACH_ERROR_INVALID_PARENT_TYPE_S', $parent_type) . ' (ERR 123)'; - throw new \Exception($errmsg, 500); - } - $parent = $apm->getAttachmentsPlugin($parent_type); - $parent_title = $parent->getTitle($parent_id, $parent_entity); - } - - // Determine the type of upload - $default_uri_type = 'file'; - $uri_type = $input->getWord('uri', $default_uri_type); - if (!in_array($uri_type, AttachmentsDefines::$LEGAL_URI_TYPES)) { - // Make sure only legal values are entered - } - - // Get the component parameters - $params = ComponentHelper::getParams('com_attachments'); - - // Set up the view - $document = $app->getDocument(); - $view = $this->getView('Add', $document->getType(), 'Administrator', ['option' => $this->option]); - - $this->addViewUrls($view, 'upload', $parent_id, $parent_type, null, $from); - // ??? Move the addViewUrls function to attachments base view class - - // We do not have a real attachment yet so fake it - $attachment = new \stdClass(); - - $attachment->uri_type = $uri_type; - $attachment->state = $params->get('publish_default', false); - $attachment->url = ''; - $attachment->url_relative = false; - $attachment->url_verify = true; - $attachment->display_name = ''; - $attachment->description = ''; - $attachment->user_field_1 = ''; - $attachment->user_field_2 = ''; - $attachment->user_field_3 = ''; - $attachment->parent_id = $parent_id; - $attachment->parent_type = $parent_type; - $attachment->parent_entity = $parent_entity; - $attachment->parent_title = $parent_title; - - $view->attachment = $attachment; - - $view->parent = $parent; - $view->new_parent = $new_parent; - $view->may_publish = $parent->userMayChangeAttachmentState($parent_id, $parent_entity, $user->id); - $view->entity_info = $entity_info; - $view->from = $from; - - $view->params = $params; - - // Display the add form - $view->display(); + $this->input->set('tmpl', 'component'); + } + + // Redirect to the edit screen. + $this->setRedirect( + Route::_( + 'index.php?option=' . $this->option + . '&view=' . $this->view_item + . $this->getRedirectToItemAppend() + . '&parent_type=' . $parentType . '.' . $parentEntity + . '&parent_id=' . $this->input->getInt('parent_id') + . '&from=' . $this->input->getString('from') + . '&editor=' . $this->input->getString('editor'), false) + ); } - - /** * Save an new attachment */ public function saveNew() { - // Check for request forgeries - Session::checkToken() or die(Text::_('JINVALID_TOKEN')); - - // Access check. - $app = $this->app; - $user = $app->getIdentity(); - if ($user === null || !$user->authorise('core.create', 'com_attachments')) { - throw new \Exception(Text::_('JERROR_ALERTNOAUTHOR') . ' (ERR 124)', 403); - } - - // Make sure we have a user - if ($user->get('username') == '') { - $errmsg = Text::_('ATTACH_ERROR_MUST_BE_LOGGED_IN_TO_UPLOAD_ATTACHMENT') . ' (ERR 125)'; - throw new \Exception($errmsg, 500); - } - - // Get the article/parent handler - $input = $this->input; - $new_parent = $input->getBool('new_parent', false); - $parent_type = $input->getCmd('parent_type', 'com_content'); - $parent_entity = $input->getCmd('parent_entity', 'default'); - - // Special handling for categories - if ($parent_type == 'com_categories') { - $parent_type = 'com_content'; - } - - // Exit if there is no Attachments plugin to handle this parent_type - PluginHelper::importPlugin('attachments'); - $apm = AttachmentsPluginManager::getAttachmentsPluginManager(); - if (!$apm->attachmentsPluginInstalled($parent_type)) { - $errmsg = Text::sprintf('ATTACH_ERROR_INVALID_PARENT_TYPE_S', $parent_type) . ' (ERR 126)'; - throw new \Exception($errmsg, 500); - } - $parent = $apm->getAttachmentsPlugin($parent_type); - $parent_entity = $parent->getCanonicalEntityId($parent_entity); - $parent_entity_name = Text::_('ATTACH_' . $parent_entity); - - // Make sure we have a valid parent ID - $parent_id = $input->getInt('parent_id', null); + // Check if plugins are enabled + if (!PluginHelper::isEnabled('attachments', 'framework')) { + $this->app->enqueueMessage(Text::_('ATTACH_WARNING_ATTACHMENTS_PLUGIN_FRAMEWORK_DISABLED'), 'error'); - if ( - !$new_parent && (($parent_id === 0) || - ($parent_id == null) || - !$parent->parentExists($parent_id, $parent_entity)) - ) { - // Warn the user to select an article/parent in a popup - $errmsg = Text::sprintf('ATTACH_ERROR_MUST_SELECT_PARENT_S', $parent_entity_name); - echo "\n"; - exit(); + return false; } - // Make sure this user has permission to upload - if (!$parent->userMayAddAttachment($parent_id, $parent_entity, $new_parent)) { - $errmsg = Text::sprintf('ATTACH_ERROR_NO_PERMISSION_TO_UPLOAD_S', $parent_entity_name) . ' (ERR 127)'; - throw new \Exception($errmsg, 403); - } + // Check for request forgeries. + Session::checkToken(); - // Set up the new record - /** @var \JMCameron\Component\Attachments\Administrator\Model\AttachmentModel $model */ - $model = $this->getModel(); - $attachment = $model->getTable(); + $model = $this->getModel(); + $data = $this->input->post->get('jform', [], 'array'); + $currentUri = (string)Uri::getInstance(); - if (!$attachment->bind($input->post->getArray())) { - $errmsg = $attachment->getError() . ' (ERR 128)'; - throw new \Exception($errmsg, 500); - } - $attachment->parent_type = $parent_type; - $parent->new = $new_parent; + if (!$this->allowSave($data)) { + $this->setMessage(Text::_('JLIB_APPLICATION_ERROR_SAVE_NOT_PERMITTED'), 'error'); - // Note the parents id and title - if ($new_parent) { - $attachment->parent_id = null; - $parent->title = ''; - } else { - $attachment->parent_id = $parent_id; - $parent->title = $parent->getTitle($parent_id, $parent_entity); + return false; } - // Upload the file! - - // Handle 'from' clause - $from = $input->getWord('from'); - - // See if we are uploading a file or URL - $new_uri_type = $input->getWord('uri_type'); - if ($new_uri_type && !in_array($new_uri_type, AttachmentsDefines::$LEGAL_URI_TYPES)) { - // Make sure only legal values are entered - $new_uri_type = ''; - } + // Validate the posted data. + $form = $model->getForm($data, false); - // If this is a URL, get settings - $verify_url = false; - $relative_url = false; - if ($new_uri_type == 'url') { - // See if we need to verify the URL (if applicable) - if ($input->getWord('verify_url') == 'verify') { - $verify_url = true; - } - // Allow relative URLs? - if ($input->getWord('url_relative') == 'relative') { - $relative_url = true; - } + if (!$form) { + throw new Exception($model->getError(), 500); } - // Update the url checkbox fields - $attachment->url_relative = $relative_url ? 1 : 0; - $attachment->url_verify = $verify_url ? 1 : 0; - - // Update create/modify info - $attachment->created_by = $user->get('id'); - $attachment->modified_by = $user->get('id'); - - PluginHelper::importPlugin('content'); - - // Upload new file/url and create the attachment - $msg = ''; - $msgType = 'message'; - $error = false; - if ($new_uri_type == 'file') { - // Set up the parent entity to save - $attachment->parent_entity = $parent_entity; + $validData = $model->validate($form, $data); - // Upload a new file - $result = AttachmentsHelper::uploadFile($attachment, $parent, false, 'upload'); - // NOTE: store() is not needed if uploadFile() is called since it does it + // Check for validation errors. + if ($validData === false) { + // Get the validation messages. + $errors = $model->getErrors(); - if (is_object($result)) { - $error = true; - $msg = $result->error_msg . ' (ERR 129)'; - $msgType = 'error'; - } else { - $msg = $result; + // Push up to three validation messages out to the user. + for ($i = 0, $n = count($errors); $i < $n && $i < 3; $i++) { + if ($errors[$i] instanceof Exception) { + $this->app->enqueueMessage($errors[$i]->getMessage(), 'warning'); + } else { + $this->app->enqueueMessage($errors[$i], 'warning'); + } } - } elseif ($new_uri_type == 'url') { - // Extra handling for checkboxes for URLs - $attachment->url_relative = $relative_url; - $attachment->url_verify = $verify_url; - // Upload/add the new URL - $result = AttachmentsHelper::addUrl($attachment, $parent, $verify_url, $relative_url); - // NOTE: store() is not needed if addUrl() is called since it does it - - if (is_object($result)) { - $error = true; - $msg = $result->error_msg . ' (ERR 130)'; - $msgType = 'error'; - } else { - $msg = $result; - } - } else { - // Set up the parent entity to save - $attachment->parent_entity = $parent_entity; + // Save the data in the session. + $this->app->setUserState('com_attachments.edit.attachment.data', $data); - $app->triggerEvent('onContentBeforeSave', [ - 'com_attachments.attachment', - $attachment, - true, - $attachment->getProperties() - ]); + // Redirect back to the same screen. + $this->setRedirect($currentUri); - // Save the updated attachment info - if (!$attachment->store()) { - $errmsg = $attachment->getError() . ' (ERR 131)'; - throw new \Exception($errmsg, 500); - } - $msg = Text::_('ATTACH_ATTACHMENT_UPDATED'); + return false; } - $app->triggerEvent('onContentAfterSave', [ - 'com_attachments.attachment', - $attachment, - true, - $attachment->getProperties() - ]); + if (!$model->save($validData)) { + // Save the data in the session. + $this->app->setUserState('com_attachments.edit.attachment.data', $data); - // See where to go to next - $task = $this->getTask(); + $this->setMessage(Text::sprintf('JLIB_APPLICATION_ERROR_SAVE_FAILED', $model->getError()), 'error'); - switch ($task) { - case 'applyNew': - if ($error) { - $link = 'index.php?option=com_attachments&task=attachment.add&parent_id=' . (int)$parent_id; - $link .= "&parent_type={$parent_type}.{$parent_entity}&editor=add_to_parent"; - } else { - $link = 'index.php?option=com_attachments&task=attachment.edit&cid[]=' . (int)$attachment->id; - } - break; - - case 'save2New': - if ($error) { - $link = 'index.php?option=com_attachments&task=attachment.add&parent_id=' . (int)$parent_id; - $link .= "&parent_type={$parent_type}.{$parent_entity}&editor=add_to_parent"; - } else { - $link = 'index.php?option=com_attachments&task=attachment.add&parent_id=' . (int)$parent_id; - $link .= "&parent_type={$parent_type}.{$parent_entity}&editor=add_to_parent"; - } - break; + // Redirect back to the edit screen. + $this->setRedirect($currentUri); - case 'saveNew': - default: - if ($error) { - $link = 'index.php?option=com_attachments&task=attachment.add&parent_id=' . (int)$parent_id; - $link .= "&parent_type={$parent_type}.{$parent_entity}&editor=add_to_parent"; - } else { - $link = 'index.php?option=com_attachments'; - } - break; - } - - // If called from the editor, go back to it - if ($from == 'editor') { - // ??? This is probably obsolete - $link = 'index.php?option=com_content&task=edit&cid[]=' . $parent_id; + return false; } - // If we are supposed to close this iframe, do it now. - if ($from == 'closeme') { - // If there has been a problem, alert the user and redisplay - if ($msgType == 'error') { - $errmsg = $msg; - if (DIRECTORY_SEPARATOR == "\\") { - // Fix filename on Windows system so alert can display it - $errmsg = str_replace(DIRECTORY_SEPARATOR, "\\\\", $errmsg); - } - $errmsg = str_replace("'", "\'", $errmsg); - $errmsg = str_replace("
", "\\n", $errmsg); - echo ""; - exit(); - } + $this->setMessage(Text::_('ATTACH_ATTACHMENT_SAVED')); - // If there is no parent_id, the parent is being created, use the username instead - if ($new_parent) { - $pid = 0; - } else { - $pid = (int)$parent_id; - } + $from = $data['from']; + if ($from === 'closeme') { // Close the iframe and refresh the attachments list in the parent window - $base_url = Uri::base(true); - $lang = $input->getCmd('lang', ''); - AttachmentsJavascript::closeIframeRefreshAttachments( - $base_url, - $parent_type, - $parent_entity, - $pid, + $uri = Uri::getInstance(); + $baseUrl = $uri->base(true); + $lang = $this->input->getCmd('lang', ''); + + AttachmentsJavascriptHelper::closeModalAndRefreshAttachments( + $baseUrl, + $validData['parent_type'], + $validData['parent_entity'], + (int)$validData[$validData['parent_type_list']], $lang, - $from + $from, + 'save' + ); + } else { + $this->setRedirect( + Route::_('index.php?option=com_attachments&view=attachments' . $this->getRedirectToListAppend(), false) ); - exit(); } - $this->setRedirect($link, $msg, $msgType); + // Clear the ancillary data from the session. + $this->app->setUserState('com_attachments.edit.attachment.data', null); + + return true; } @@ -538,201 +268,11 @@ public function edit($key = null, $urlVar = null) throw new \Exception(Text::_('ATTACH_ERROR_NO_PERMISSION_TO_EDIT') . ' (ERR 132)', 403); } - $uri = Uri::getInstance(); - - /** @var \JMCameron\Component\Attachments\Administrator\Model\AttachmentModel $model */ - $model = $this->getModel(); - $attachment = $model->getTable(); - - $input = $app->getInput(); - $cid = $input->get('cid', array(0), 'array'); - $change = $input->getWord('change', ''); - $change_parent = ($change == 'parent'); - $attachment_id = (int)$cid[0]; - - // Get the attachment data - $attachment = $model->getItem($attachment_id); - - $from = $input->getWord('from'); - $layout = $input->getWord('tmpl'); - - // Fix the URL for files - if ($attachment->uri_type == 'file') { - $attachment->url = $uri->root(true) . '/' . $attachment->url; - } - - $parent_id = $attachment->parent_id; - $parent_type = $attachment->parent_type; - $parent_entity = $attachment->parent_entity; - - // Get the parent handler - PluginHelper::importPlugin('attachments'); - $apm = AttachmentsPluginManager::getAttachmentsPluginManager(); - if (!$apm->attachmentsPluginInstalled($parent_type)) { - // Exit if there is no Attachments plugin to handle this parent_type - $errmsg = Text::sprintf('ATTACH_ERROR_INVALID_PARENT_TYPE_S', $parent_type) . ' (ERR 133)'; - throw new \Exception($errmsg, 500); - } - $entity_info = $apm->getInstalledEntityInfo(); - $parent = $apm->getAttachmentsPlugin($parent_type); - - // Get the parent info - $parent_entity_name = Text::_('ATTACH_' . $parent_entity); - $parent_title = $parent->getTitle($parent_id, $parent_entity); - if (!$parent_title) { - $parent_title = Text::sprintf('ATTACH_NO_PARENT_S', $parent_entity_name); - } - $attachment->parent_entity_name = $parent_entity_name; - $attachment->parent_title = $parent_title; - $attachment->parent_published = $parent->isParentPublished($parent_id, $parent_entity); - $update = $input->getWord('update'); - if ($update && !in_array($update, AttachmentsDefines::$LEGAL_URI_TYPES)) { - $update = false; - } - - // Set up view for changing parent - $document = $app->getDocument(); - if ($change_parent) { - $js = " - function jSelectParentArticle(id, title) { - document.getElementById('parent_id').value = id; - document.getElementById('parent_title').value = title; - window.parent.bootstrap.Modal.getInstance(window.parent.document.querySelector('.joomla-modal.show')).hide(); - };" ; - $document->addScriptDeclaration($js); - } - - // See if a new type of parent was requested - $new_parent_type = ''; - $new_parent_entity = 'default'; - $new_parent_entity_name = ''; - if ($change_parent) { - $new_parent_type = $input->getCmd('new_parent_type'); - if ($new_parent_type) { - if (strpos($new_parent_type, '.')) { - $parts = explode('.', $new_parent_type); - $new_parent_type = $parts[0]; - $new_parent_entity = $parts[1]; - } - - $new_parent = $apm->getAttachmentsPlugin($new_parent_type); - $new_parent_entity = $new_parent->getCanonicalEntityId($new_parent_entity); - $new_parent_entity_name = Text::_('ATTACH_' . $new_parent_entity); - - // Set up the 'select parent' button - $selpar_label = Text::sprintf('ATTACH_SELECT_ENTITY_S_COLON', $new_parent_entity_name); - $selpar_btn_text = ' ' . - Text::sprintf( - 'ATTACH_SELECT_ENTITY_S', - $new_parent_entity_name - ) . ' '; - $selpar_btn_tooltip = Text::sprintf('ATTACH_SELECT_ENTITY_S_TOOLTIP', $new_parent_entity_name); - - $selpar_btn_url = $new_parent->getSelectEntityURL($new_parent_entity); - $selpar_parent_title = ''; - $selpar_parent_id = '-1'; - } else { - // Set up the 'select parent' button - $selpar_label = Text::sprintf('ATTACH_SELECT_ENTITY_S_COLON', $attachment->parent_entity_name); - $selpar_btn_text = ' ' . - Text::sprintf('ATTACH_SELECT_ENTITY_S', $attachment->parent_entity_name) . ' '; - $selpar_btn_tooltip = Text::sprintf('ATTACH_SELECT_ENTITY_S_TOOLTIP', $attachment->parent_entity_name); - $selpar_btn_url = $parent->getSelectEntityURL($parent_entity); - $selpar_parent_title = $attachment->parent_title; - $selpar_parent_id = $attachment->parent_id; - } - } - - $change_parent_url = $uri->base(true) . - "/index.php?option=com_attachments&task=attachment.edit&cid[]=$attachment_id&change=parent"; - if ($layout) { - $change_parent_url .= "&from=$from&tmpl=$layout"; - } - - // Get the component parameters - $params = ComponentHelper::getParams('com_attachments'); - - // Set up the view - $document = $app->getDocument(); - $view = $this->getView('Edit', $document->getType(), 'Administrator', ['option' => $this->option]); - - $this->addViewUrls( - $view, - 'update', - $parent_id, - $parent_type, - $attachment_id, - $from - ); - - // Update change URLS to remember if we want to change the parent - if ($change_parent) { - $view->change_file_url .= "&change=parent&new_parent_type=$new_parent_type"; - $view->change_url_url .= "&change=parent&new_parent_type=$new_parent_type"; - $view->normal_update_url .= "&change=parent&new_parent_type=$new_parent_type"; - if ($new_parent_entity != 'default') { - $view->change_file_url .= ".$new_parent_entity"; - $view->change_url_url .= ".$new_parent_entity"; - $view->normal_update_url .= ".$new_parent_entity"; - } - } - - // Add a few necessary things for iframe popups - if ($layout) { - $view->change_file_url .= "&from=$from&tmpl=$layout"; - $view->change_url_url .= "&from=$from&tmpl=$layout"; - $view->normal_update_url .= "&from=$from&tmpl=$layout"; - } - - // Suppress the display filename if we are switching from file to url - $display_name = $attachment->display_name; - if ($update && ($update != $attachment->uri_type)) { - $attachment->display_name = ''; - } + // TODO remove this hack in next revision + // default parent::edit() expects cid sent by POST request + $this->input->post->set('cid', $this->input->get('cid', [], 'int')); - // Handle iframe popup requests - $known_froms = $parent->knownFroms(); - $in_popup = false; - $save_url = 'index.php'; - if (in_array($from, $known_froms)) { - $in_popup = true; - AttachmentsJavascript::setupJavascript(); - $save_url = 'index.php?option=com_attachments&task=attachment.save'; - } - $view->save_url = $save_url; - $view->in_popup = $in_popup; - - // Set up the access field - $view->access_level_tooltip = Text::_('JFIELD_ACCESS_LABEL') . '::' . Text::_('JFIELD_ACCESS_DESC'); - $view->access_level = AccessLevelsField::getAccessLevels('access', 'access', $attachment->access); - - // Set up view info - $view->update = $update; - $view->change_parent = $change_parent; - $view->new_parent_type = $new_parent_type; - $view->new_parent_entity = $new_parent_entity; - $view->change_parent_url = $change_parent_url; - $view->entity_info = $entity_info; - $view->may_publish = $parent->userMayChangeAttachmentState($parent_id, $parent_entity, $user->id); - - $view->from = $from; - - $view->attachment = $attachment; - - $view->parent = $parent; - $view->params = $params; - - // Set up for selecting a new type of parent - if ($change_parent) { - $view->selpar_label = $selpar_label; - $view->selpar_btn_text = $selpar_btn_text; - $view->selpar_btn_tooltip = $selpar_btn_tooltip; - $view->selpar_btn_url = $selpar_btn_url; - $view->selpar_parent_title = $selpar_parent_title; - $view->selpar_parent_id = $selpar_parent_id; - } - - $view->display(); + return parent::edit(); } @@ -1172,9 +712,6 @@ public function download() AttachmentsHelper::downloadAttachment($id); } - - - /** * Put up a dialog to double-check before deleting an attachment */ @@ -1248,6 +785,7 @@ public function deleteWarning() public function cancel($key = null) { + $this->app->setUserState('com_attachments.edit.attachment.data', null); $this->setRedirect(Route::_('index.php?option=' . $this->option, false)); } } diff --git a/attachments_component/admin/src/Helper/AttachmentsUploadHelper.php b/attachments_component/admin/src/Helper/AttachmentsUploadHelper.php new file mode 100644 index 00000000..cfaa8866 --- /dev/null +++ b/attachments_component/admin/src/Helper/AttachmentsUploadHelper.php @@ -0,0 +1,1383 @@ +uri_type == 'url') { + // Do not need to do any file operations if this is a URL + return ''; + } + + // Parent wasn't change + if (($attachment->parent_id_old == $newParentId) + && ($attachment->parent_type_old == $newParentType) + && ($attachment->parent_entity_old == $newParentEntity)) { + return ''; + } + + // Get the article/parent handler + if ($newParentType) { + $parentType = $newParentType; + $parentEntity = $newParentEntity; + } else { + $parentType = $attachment->parent_type; + $parentEntity = $attachment->parent_entity; + } + + if (!PluginHelper::importPlugin('attachments')) { + // Exit if the framework does not exist (eg, during uninstallation) + return ''; + } + + $apm = AttachmentsPluginManager::getAttachmentsPluginManager(); + if (!$apm->attachmentsPluginInstalled($parentType)) { + return Text::sprintf('ATTACH_ERROR_UNKNOWN_PARENT_TYPE_S', $parentType) . ' (ERR 45)'; + } + + $parent = $apm->getAttachmentsPlugin($parentType); + + // Set up the entity name for display + $parentEntity = $parent->getCanonicalEntityName($parentEntity); + $parentEntityName = Text::_('ATTACH_' . $parentEntity); + + // Get the component parameters + $params = ComponentHelper::getParams('com_attachments'); + + // Define where the attachments move to + $uploadUrl = AttachmentsDefines::$ATTACHMENTS_SUBDIR; + $uploadDir = JPATH_SITE . '/' . $uploadUrl; + + // Figure out the new system filename + $newPath = $parent->getAttachmentPath($parentEntity, $newParentId, 0); + $newFullpath = $uploadDir . '/' . $newPath; + + // Make sure the new directory exists + if (!Folder::create($newFullpath)) { + return Text::sprintf('ATTACH_ERROR_UNABLE_TO_CREATE_DIR_S', $newFullpath) . ' (ERR 46)'; + } + + // Construct the new filename and URL + $oldFilenameSys = $attachment->filename_sys; + $newFilenameSys = $newFullpath . $attachment->filename; + $newUrl = StringHelper::str_ireplace( + DIRECTORY_SEPARATOR, + '/', + $uploadUrl . '/' . $newPath . $attachment->filename + ); + + // Rename the file + if (is_file($newFilenameSys)) { + return Text::sprintf( + 'ATTACH_ERROR_CANNOT_SWITCH_PARENT_S_NEW_FILE_S_ALREADY_EXISTS', + $parentEntityName, + $attachment->filename + ); + } + + if (!File::move($oldFilenameSys, $newFilenameSys)) { + $newFilename = $newPath . $attachment->filename; + + return Text::sprintf( + 'ATTACH_ERROR_CANNOT_SWITCH_PARENT_S_RENAMING_FILE_S_FAILED', + $parentEntityName, + $newFilename + ); + } + + self::writeEmptyIndexHtml($newFullpath); + + // Save the changes to the attachment record immediately + $attachment->parent_id = $newParentId; + $attachment->parent_entity = $parentEntity; + $attachment->parentEntityName = $parentEntityName; + $attachment->filename_sys = $newFilenameSys; + $attachment->url = $newUrl; + + // Clean up after ourselves + self::cleanDirectory($oldFilenameSys); + + return ''; + } + + /** + * Truncate the filename if it is longer than the maxlen + * Do this by deleting necessary at the end of the base filename (before the extensions) + * + * @access protected + * + * @param string $rawFilename the input filename + * @param int $maxlen the maximum allowed length (0 means no limit) + * + * @return string the truncated filename + * + * @since 1.0.0 + */ + protected static function truncateFilename(string $rawFilename, int $maxlen): string + { + // Do not truncate if $maxlen is 0 or no truncation is needed + if (($maxlen == 0) || (strlen($rawFilename) <= $maxlen)) { + return $rawFilename; + } + + $filenameInfo = pathinfo($rawFilename); + $basename = $filenameInfo['basename']; + $filename = $filenameInfo['filename']; + + $extension = ''; + + if ($basename != $filename) { + $extension = $filenameInfo['extension']; + } + + if (strlen($extension) > 0) { + $maxlen = max($maxlen - (strlen($extension) + 2), 1); + + return substr($filename, 0, $maxlen) . '~.' . $extension; + } else { + $maxlen = max($maxlen - 1, 1); + + return substr($filename, 0, $maxlen) . '~'; + } + } + + /** + * Make sure this a valid image file + * + * @access public + * + * @param string $filepath the full path to the image file + * + * @return bool true if it is a valid image file + * + * @since 1.0.0 + */ + public static function isValidImageFile(string $filepath): bool + { + return getimagesize($filepath) !== false; + } + + /** + * Determine if a file is an image file + * + * Adapted from com_media + * + * @access public + * + * @param string $filename the filename to check + * + * @return bool true if it is an image file + * + * @since 1.0.0 + */ + public static function isImageFile(string $filename): bool + { + // Partly based on PHP getimagesize documentation for PHP 7.0+ + static $imageTypes = 'xcf|odg|gif|jpg|jpeg|png|bmp|psd|tiff|swc|iff|jpc|jp2|jpx|jb2|xbm|wbmp|ico|webp'; + + return preg_match("/\.(?:$imageTypes)$/i", $filename); + } + + /** + * Make sure a file is not a double-extension exploit + * See: https://www.acunetix.com/websitesecurity/upload-forms-threat/ + * + * @access public + * + * @param string $filename the filename + * + * @return bool true if it is an exploit file + * + * @since 1.0.0 + */ + public static function isDoubleExtensionExploit(string $filename): bool + { + return preg_match("/\.php\.[a-z0-9]+$/i", $filename); + } + + /** + * Write an empty 'index.html' file in the specified directory to prevent snooping + * + * @access public + * + * @param string $dir full path of the directory needing an 'index.html' file + * + * @return bool true if the file was successfully written + * + * @since 1.0.0 + */ + public static function writeEmptyIndexHtml(string $dir): bool + { + $indexFname = $dir . '/index.html'; + + if (is_file($indexFname)) { + return true; + } + + $contents = "

Access denied.

"; + File::write($indexFname, $contents); + + return is_file($indexFname); + } + + /** + * Set up the upload directory + * + * @access public + * + * @param string $uploadDir the directory to be set up + * @param bool $secure true if the directory should be set up for secure mode (with the necessary .htaccess file) + * + * @return bool true if successful + * + * @throws Exception + * + * @since 1.0.0 + */ + public static function setupUploadDirectory(string $uploadDir, bool $secure): bool + { + $subdirOk = false; + + // Do not allow the main site directory to be set up as the upload directory + $direndChars = DIRECTORY_SEPARATOR . '/'; + + if ((realpath(rtrim($uploadDir, $direndChars)) == realpath(JPATH_SITE)) + || (realpath(rtrim($uploadDir, $direndChars)) == realpath(JPATH_ADMINISTRATOR))) { + $errMsg = Text::sprintf('ATTACH_ERROR_UNABLE_TO_SETUP_UPLOAD_DIR_S', $uploadDir) . ' (ERR 29)'; + Factory::getApplication()->enqueueMessage($errMsg, 'error'); + } + + // Create the subdirectory (if necessary) + if (is_dir($uploadDir)) { + $subdirOk = true; + } else { + if (Folder::create($uploadDir)) { + // ??? Change to 2775 if files are owned by you but webserver runs as group + // ??? (Should the permission be an option?) + chmod($uploadDir, 0775); + $subdirOk = true; + } + } + + if (!$subdirOk || !is_dir($uploadDir)) { + $errMsg = Text::sprintf('ATTACH_ERROR_UNABLE_TO_SETUP_UPLOAD_DIR_S', $uploadDir) . ' (ERR 30)'; + Factory::getApplication()->enqueueMessage($errMsg, 'error'); + } + + // Add a simple index.html file to the upload directory to prevent browsing + if (!self::writeEmptyIndexHtml($uploadDir)) { + $errMsg = Text::sprintf('ATTACH_ERROR_ADDING_INDEX_HTML_IN_S', $uploadDir) . ' (ERR 31)'; + Factory::getApplication()->enqueueMessage($errMsg, 'error'); + } + + // If this is secure, create the .htindex file, if necessary + $htaFname = $uploadDir . '/.htaccess'; + + if ($secure) { + $htaOk = false; + + $line = "order deny,allow\ndeny from all\n"; + File::write($htaFname, $line); + + if (is_file($htaFname)) { + $htaOk = true; + } + + if (!$htaOk) { + $errMsg = Text::sprintf('ATTACH_ERROR_ADDING_HTACCESS_S', $uploadDir) . ' (ERR 32)'; + Factory::getApplication()->enqueueMessage($errMsg, 'error'); + } + } else { + if (is_file($htaFname)) { + // If the htaccess file exists, delete it so normal access can occur + File::delete($htaFname); + } + } + + return true; + } + + /** + * Upload the file + * + * @access public + * + * @param &object $partialAttachment the partially constructed attachment object + * @param &object $parent an Attachments plugin parent object with partial parent info including: + * $parent->new : True if the parent has not been created yet + * (like adding attachments to an article before it has been saved) + * $parent->title : Title/name of the parent object + * @param array $files uploaded files info array + * @param bool $new 'upload' or 'update' + * + * @return string a message indicating succes or failure + * + * @throws Exception + * + * @since 1.0.0 + * + * NOTE: The caller should set up all the parent info in the record before calling this + * (see $parent->* below for necessary items) + */ + public static function uploadFile(&$partialAttachment, &$parent, array $files, bool $new = false): string + { + $file = $files['filename']; + + $app = Factory::getApplication(); + $user = $app->getIdentity(); + $db = Factory::getContainer()->get('DatabaseDriver'); + + // Figure out if the user may publish this attachment + $mayPublish = $parent->userMayChangeAttachmentState( + (int)$partialAttachment->parent_id, + $partialAttachment->parent_entity, + (int)$partialAttachment->created_by + ); + + // Get the component parameters + $params = ComponentHelper::getParams('com_attachments'); + + // Make sure the attachments directory exists + $uploadDir = JPATH_SITE . '/' . AttachmentsDefines::$ATTACHMENTS_SUBDIR; + $secure = $params->get('secure', false); + + if (!self::setupUploadDirectory($uploadDir, $secure)) { + $errMsg = Text::sprintf('ATTACH_ERROR_UNABLE_TO_SETUP_UPLOAD_DIR_S', $uploadDir) . ' (ERR 33)'; + Factory::getApplication()->enqueueMessage($errMsg, 'error'); + + return $errMsg; + } + + // If we are updating, note the name of the old filename + $oldFilename = null; + $oldFilenameSys = null; + + if ($partialAttachment->uri_type) { + $oldFilename = $partialAttachment->filename; + $oldFilenameSys = $partialAttachment->filename_sys; + } + + /** + * Get the new filename + * (Note: The following replacement is necessary to allow single quotes in filenames to work correctly.) + * Trim of any trailing period (to avoid exploits) + */ + $filename = rtrim(str_ireplace("\'", "'", $file['name']), '.'); + $ftype = $file['type']; + + // Check the file size + $maxUploadSize = (int)ini_get('upload_max_filesize'); + $maxAttachmentSize = (int)$params->get('max_attachment_size', 0); + + if ($maxAttachmentSize == 0) { + $maxAttachmentSize = $maxUploadSize; + } + + $maxSize = min($maxUploadSize, $maxAttachmentSize); + $fileSize = filesize($file['tmp_name']) / 1048576.0; + + if ($fileSize > $maxSize) { + return Text::sprintf( + 'ATTACH_ERROR_FILE_S_TOO_BIG_N_N_N', + $filename, + $fileSize, + $maxAttachmentSize, + $maxUploadSize + ); + } + + // Get the maximum allowed filename length (for the filename display) + $maxFilenameLength = (int)$params->get('max_filename_length', 0); + + if ($maxFilenameLength == 0) { + $maxFilenameLength = AttachmentsDefines::$MAXIMUM_FILENAME_LENGTH; + } else { + $maxFilenameLength = min($maxFilenameLength, AttachmentsDefines::$MAXIMUM_FILENAME_LENGTH); + } + + // Truncate the filename, if necessary and alert the user + if (strlen($filename) > $maxFilenameLength) { + $filename = self::truncateFilename($filename, $maxFilenameLength); + $msg = Text::_('ATTACH_WARNING_FILENAME_TRUNCATED'); + + if ($app->isClient('administrator')) { + $lang = $app->getLanguage(); + + if ($lang->isRTL()) { + $msg = "'$filename' " . $msg; + } else { + $msg = $msg . " '$filename'"; + } + + $app->enqueueMessage($msg, 'warning'); + } else { + $msg .= "\\n \'$filename\'"; + + echo AttachmentsJavascript::alertMsg($msg); + } + } + + // Check the filename for bad characters + $badChar = ''; + $badChars = false; + $forbiddenChars = $params->get('forbidden_filename_characters', '#=?%&'); + + for ($i = 0; $i < strlen($forbiddenChars); $i++) { + $char = $forbiddenChars[$i]; + + if (strpos($filename, $char) !== false) { + $badChar = $char; + $badChars = true; + break; + } + } + + $badFilename = false; + + /* + * This was tested before in AttachmentModel::validateUploadFile. + * // Check for double-extension exploit and other security anomalies + * $badFilename = !InputFilter::isSafeFile($files); + */ + + // Set up the entity name for display + $parentEntity = $parent->getCanonicalEntityName($partialAttachment->parent_entity); + $parentEntityName = Text::_('ATTACH_' . $parentEntity); + + // A little formatting + $msgbreak = '
'; + + $from = $app->input->getWord('from'); + + // Make sure a file was successfully uploaded + if ((($file['size'] == 0) && ($file['tmp_name'] == '')) || $badChars || $badFilename) { + // Guess the type of error + if ($badChars) { + $errMsg = Text::sprintf('ATTACH_ERROR_BAD_CHARACTER_S_IN_FILENAME_S', $badChar, $filename); + } elseif ($badFilename) { + $format = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + $errMsg = Text::_('ATTACH_ERROR_ILLEGAL_FILE_EXTENSION') . " .php.$format"; + } elseif ($filename == '') { + $errMsg = Text::sprintf('ATTACH_ERROR_UPLOADING_FILE_S', $filename); + $errMsg .= $msgbreak . ' (' . Text::_('ATTACH_YOU_MUST_SELECT_A_FILE_TO_UPLOAD') . ')'; + } else { + $errMsg = Text::sprintf('ATTACH_ERROR_UPLOADING_FILE_S', $filename); + $errMsg .= $msgbreak . '(' . Text::_('ATTACH_ERROR_MAY_BE_LARGER_THAN_LIMIT') . ' '; + $errMsg .= get_cfg_var('upload_max_filesize') . ')'; + } + + if ($errMsg != '') { + if ($app->isClient('administrator')) { + return $errMsg; + } + } + } + + // Make sure the file type is okay (respect restrictions imposed by media manager) + $cmparams = ComponentHelper::getParams('com_media'); + + // Check to make sure the extension is allowed + $restrictUploadsExtensions = explode(',', $cmparams->get('restrict_uploads_extensions')); + $imageExtensions = explode(',', $cmparams->get('image_extensions')); + $audioExtensions = explode(',', $cmparams->get('audio_extensions')); + $videoExtensions = explode(',', $cmparams->get('video_extensions')); + $docExtensions = explode(',', $cmparams->get('doc_extensions')); + $ignoreExtensions = explode(',', $cmparams->get('ignore_extensions')); + + $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION)); + + if (!in_array($extension, $restrictUploadsExtensions) && !in_array($extension, $ignoreExtensions)) { + $errMsg = Text::sprintf('ATTACH_ERROR_UPLOADING_FILE_S', $filename); + $errMsg .= $msgbreak . Text::_('ATTACH_ERROR_ILLEGAL_FILE_EXTENSION') . " $extension"; + + if ($user->authorise('core.admin')) { + $errMsg .= $msgbreak . Text::_('ATTACH_ERROR_CHANGE_IN_MEDIA_MANAGER'); + } + + return $errMsg; + } + + // Check to make sure the mime type is okay + if ($cmparams->get('restrict_uploads', true)) { + if ($cmparams->get('check_mime', true)) { + $allowedMime = explode(',', $cmparams->get('upload_mime')); + $illegalMime = explode(',', $cmparams->get('upload_mime_illegal')); + + if (strlen($ftype) && !in_array($ftype, $allowedMime) && in_array($ftype, $illegalMime)) { + $errMsg = Text::sprintf('ATTACH_ERROR_UPLOADING_FILE_S', $filename); + $errMsg .= $msgbreak . Text::_('ATTACH_ERROR_ILLEGAL_FILE_MIME_TYPE') . " $ftype"; + + if ($user->authorise('core.admin')) { + $errMsg .= $msgbreak . Text::_('ATTACH_ERROR_CHANGE_IN_MEDIA_MANAGER'); + } + + return $errMsg; + } + } + } + + // If it is an image file, make sure it is a valid image file (and not some kind of exploit) + if (self::isImageFile($filename)) { + if (!self::isValidImageFile($file['tmp_name'])) { + if (!in_array($extension, $imageExtensions)) { + $errMsg = Text::sprintf('ATTACH_ERROR_UPLOADING_FILE_S', $filename); + $errMsg .= "
" . Text::_('ATTACH_ERROR_ILLEGAL_FILE_CORRUPTED_IMAGE_FILE_S'); + + return $errMsg; + } + } + } + + // Handle PDF mime types + if ($extension == 'pdf') { + if (in_array($ftype, AttachmentsFileTypes::$attachments_pdf_mime_types)) { + $ftype = 'application/pdf'; + } + } + + // Define where the attachments go + $uploadUrl = AttachmentsDefines::$ATTACHMENTS_SUBDIR; + $uploadDir = JPATH_SITE . '/' . $uploadUrl; + + // Figure out the system filename + $path = $parent->getAttachmentPath($partialAttachment->parent_entity, $partialAttachment->parent_id, 0); + $fullpath = $uploadDir . '/' . $path; + + // Make sure the directory exists + if (!is_file($fullpath)) { + if (!Folder::create($fullpath)) { + return Text::sprintf('ATTACH_ERROR_UNABLE_TO_SETUP_UPLOAD_DIR_S', $uploadDir) . ' (ERR 34)'; + } + + self::writeEmptyIndexHtml($fullpath); + } + + // Get ready to save the file + $filenameSys = $fullpath . $filename; + + $url = $uploadUrl . '/' . $path . $filename; + + $baseUrl = Uri::getInstance()->base(); + + // If we are on windows, fix the filename and URL + if (DIRECTORY_SEPARATOR != '/') { + $filenameSys = str_replace('/', DIRECTORY_SEPARATOR, $filenameSys); + $url = str_replace(DIRECTORY_SEPARATOR, '/', $url); + } + + // Check on length of filenameSys + if (strlen($filenameSys) > AttachmentsDefines::$MAXIMUM_FILENAME_SYS_LENGTH) { + return Text::sprintf( + 'ATTACH_ERROR_FILEPATH_TOO_LONG_N_N_S', + strlen($filenameSys), + AttachmentsDefines::$MAXIMUM_FILENAME_SYS_LENGTH, + $filename + ) . '(ERR 35)'; + } + + // Make sure the system filename doesn't already exist + $duplicateFilename = false; + + if ($new && is_file($filenameSys)) { + // Cannot overwrite an existing file when creating a new attachment! + $duplicateFilename = true; + } + + if (!$new && is_file($filenameSys)) { + // If updating, we may replace the existing file but may not overwrite any other existing file + $query = $db + ->getQuery(true) + ->select($db->qn('id')) + ->from($db->qn('#__attachments')) + ->where([$db->qn('filename_sys') . ' = :filename_sys', $db->qn('id') . ' != :id']) + ->bind(':filename_sys', $filenameSys, ParameterType::STRING) + ->bind(':id', $partialAttachment->id, ParameterType::INTEGER); + + try { + $duplicateFilename = $db + ->setQuery($query, 0, 1) + ->loadResult() > 0; + } catch (RuntimeException $e) { + } + } + + // Handle duplicate filename error + if ($duplicateFilename) { + $errMsg = Text::sprintf('ATTACH_ERROR_FILE_S_ALREADY_ON_SERVER', $filename); + + if ($app->isClient('administrator')) { + return $errMsg; + } + } + + // Create a display filename, if needed (for long filenames) + if (($maxFilenameLength > 0) + && (strlen($partialAttachment->display_name) == 0) + && (strlen($filename) > $maxFilenameLength)) { + $partialAttachment->display_name = self::truncateFilename($filename, $maxFilenameLength); + } + + // Copy the info about the uploaded file into the new record + $partialAttachment->uri_type = 'file'; + $partialAttachment->filename = $filename; + $partialAttachment->filename_sys = $filenameSys; + $partialAttachment->url = $url; + $partialAttachment->file_type = $ftype; + $partialAttachment->file_size = $file['size']; + + // If the user is not authorised to change the state (eg, publish/unpublish), + // ignore the form data and make sure the publish state is set correctly. + if (!$mayPublish) { + if ($new) { + // Use the default publish state (ignore form info) + $params = ComponentHelper::getParams('com_attachments'); + $partialAttachment->state = $params->get('publish_default', false); + } else { + // Restore the old state (ignore form info) + $query = $db + ->getQuery(true) + ->select($db->qn('state')) + ->from($db->qn('#__attachments')) + ->where($db->qn('id') . ' = :id') + ->bind(':id', $partialAttachment->id, ParameterType::INTEGER); + + try { + $oldState = $db + ->setQuery($query, 0, 1) + ->loadResult(); + } catch (RuntimeException $e) { + $errMsg = $db->stderr() . ' (ERR 36)'; + Factory::getApplication()->enqueueMessage($errMsg, 'error'); + + return $errMsg; + } + + $partialAttachment->state = $oldState; + } + } + + // Add the icon file type if not set by control + if (!$partialAttachment->icon_filename) { + $partialAttachment->icon_filename = AttachmentsFileTypes::iconFilename($filename, $ftype); + } + + // Save the updated attachment + if (!$partialAttachment->store()) { + return Text::_('ATTACH_ERROR_SAVING_FILE_ATTACHMENT_RECORD') . $partialAttachment->getError() . ' (ERR 37)'; + } + + // Move the file + $msg = ""; + + $useStreams = false; + $allowUnsafe = $params->get('test_is_safe_file'); + $uploadedOk = File::upload($file['tmp_name'], $filenameSys, $useStreams, $allowUnsafe); + + if ($uploadedOk) { + $fileSize = (int)($partialAttachment->file_size / 1024.0); + $fileSizeStr = Text::sprintf('ATTACH_S_KB', $fileSize); + + chmod($filenameSys, 0644); + + // ??? The following items need to be updated for RTL + if (!$new) { + $msg = Text::_('ATTACH_UPDATED_ATTACHMENT') . ' ' . $filename . ' (' . $fileSizeStr . ')!'; + } else { + $msg = Text::_('ATTACH_UPLOADED_ATTACHMENT') . ' ' . $filename . ' (' . $fileSizeStr . ')!'; + } + } else { + $query = $db + ->getQuery(true) + ->delete($db->qn('#__attachments')) + ->where($db->qn('id') . ' = :id') + ->bind(':id', $partialAttachment->id, ParameterType::INTEGER); + + try { + $db + ->setQuery($query) + ->execute(); + } catch (RuntimeException $e) { + $errMsg = $db->stderr() . ' (ERR 38)'; + Factory::getApplication()->enqueueMessage($errMsg, 'error'); + + return $errMsg; + } + + $msg = Text::_('ATTACH_ERROR_MOVING_FILE') . " {$file['tmp_name']} -> $filenameSys)"; + } + + // If we are updating, we may need to delete the old file + if (!$new) { + if (($filenameSys != $oldFilenameSys) && is_file($oldFilenameSys)) { + File::delete($oldFilenameSys); + self::cleanDirectory($oldFilenameSys); + } + } + + return ''; + } + + /** + * Download an attachment (in secure mode) + * + * @access public + * + * @param int $id the attachment id + * + * @return void + * + * @throws Exception + * + * @since 1.0.0 + */ + public static function downloadAttachment(int $id): void + { + $baseUrl = Uri::getInstance()->base(); + + $app = Factory::getApplication(); + + if ($app->isClient('administrator')) { + $model = $app->bootComponent('com_attachments')->getMVCFactory() + ->createModel('Attachment', 'Administrator', ['ignore_request' => true]); + } else { + $model = $app->bootComponent('com_attachments')->getMVCFactory() + ->createModel('Attachment', 'Site', ['ignore_request' => true]); + } + + $attachment = $model->getItem($id); + + if (!$attachment) { + $errMsg = Text::sprintf('ATTACH_ERROR_INVALID_ATTACHMENT_ID_N', $id) . ' (ERR 41)'; + $app->enqueueMessage($errMsg, 'error'); + } + + $parentId = $attachment->parent_id; + $parentType = $attachment->parent_type; + $parentEntity = $attachment->parent_entity; + + // Get the article/parent handler + PluginHelper::importPlugin('attachments'); + $apm = AttachmentsPluginManager::getAttachmentsPluginManager(); + + if (!$apm->attachmentsPluginInstalled($parentType)) { + $errMsg = Text::sprintf('ATTACH_ERROR_UNKNOWN_PARENT_TYPE_S', $parentType) . ' (ERR 42)'; + $app->enqueueMessage($errMsg, 'error'); + } + + $parent = $apm->getAttachmentsPlugin($parentType); + + // Get the component parameters + $params = ComponentHelper::getParams('com_attachments'); + + // Make sure that the user can access the attachment + if (!$parent->userMayAccessAttachment($attachment)) { + // If not logged in, warn them to log in + $user = $app->getIdentity(); + + if ($user->get('username') == '') { + $guestLevels = $params->get('show_guest_access_levels', ['1']); + + if (in_array($attachment->access, $guestLevels)) { + // Construct the login request with return URL + $return = $app->getUserState('com_attachments.current_url', ''); + $redirectTo = Route::_( + $baseUrl . 'index.php?option=com_attachments&task=attachment.requestLogin' . $return + ); + $app->redirect($redirectTo); + } + } + + // Otherwise, just error out + $errMsg = Text::_('ATTACH_ERROR_NO_PERMISSION_TO_DOWNLOAD') . ' (ERR 43)'; + $app->enqueueMessage($errMsg, 'error'); + } + + // Get the other info about the attachment + $downloadMode = $params->get('download_mode', 'attachment'); + $contentType = $attachment->file_type; + + if ($attachment->uri_type == 'file') { + $filename = $attachment->filename; + $filenameSys = $attachment->filename_sys; + + // Make sure the file exists + if (!is_file($filenameSys)) { + $errMsg = Text::sprintf('ATTACH_ERROR_FILE_S_NOT_FOUND_ON_SERVER', $filename) . ' (ERR 44)'; + $app->enqueueMessage($errMsg, 'error'); + } + + $fileSize = filesize($filenameSys); + + // Construct the downloaded filename + $filenameInfo = pathinfo($filename); + $extension = "." . $filenameInfo['extension']; + $basename = basename($filename, $extension); + /** + * Modify the following line insert a string into + * the filename of the downloaded file, for example: + * $modFilename = $basename . "(yoursite)" . $extension; + */ + $modFilename = $basename . $extension; + + // No need to update counter when in backend. + if (!$app->isClient('administrator')) { + $model->incrementDownloadCount(); + } + + // Begin writing headers + // Clear any previously written headers in the output buffer + ob_clean(); + + // Handle MSIE differently... + $browser = Browser::getInstance(); + $browserType = $browser->getBrowser(); + $browserVersion = $browser->getMajor(); + + // Handle older versions of MS Internet Explorer + if (($browserType == 'msie') && ($browserVersion <= 8)) { + // Ensure UTF8 characters in filename are encoded correctly in IE + $modFilename = rawurlencode($modFilename); + + // Tweak the headers for MSIE + header('Pragma: private'); + header('Cache-control: private, must-revalidate'); + } else { + header('Cache-Control: private, max-age=0, must-revalidate, no-store'); + } + + header("Content-Length: " . $fileSize); + + // Force the download + if ($downloadMode == 'attachment') { + // Attachment + header("Content-Disposition: attachment; filename=\"$modFilename\""); + } else { + // Inline + header("Content-Disposition: inline; filename=\"$modFilename\""); + } + + header('Content-Transfer-Encoding: binary'); + header("Content-Type: $contentType"); + + // If x-sendfile is available, use it + $usingSsl = strtolower(substr($baseUrl, 0, 5)) == 'https'; + + if (!$usingSsl && function_exists('apache_get_modules') && in_array( + 'mod_xsendfile', + apache_get_modules() + )) { + header("X-Sendfile: $filenameSys"); + } else { + if ($fileSize <= 1048576) { + // If the file size is one MB or less, use readfile + // ??? header("Content-Length: ".$fileSize); + @readfile($filenameSys); + } else { + // Send it in 8K chunks + set_time_limit(0); + $file = @fopen($filenameSys, "rb"); + + while (!feof($file) && (connection_status() == 0)) { + print(@fread($file, 8 * 1024)); + ob_flush(); + flush(); + } + } + } + + exit; + } else { + if ($attachment->uri_type == 'url') { + // Note the download + $model->incrementDownloadCount(); + + // Forward to the URL + // Clear any previously written headers in the output buffer + ob_clean(); + header("Location: $attachment->url"); + } + } + } + + /** + * Parse the url into parts + * + * @access private + * + * @param string &$rawUrl the raw url to parse + * @param bool $relativeUrl allow relative URLs + * + * @return object an object (if successful) with the parts as attributes (or an error string in case of error) + * + * @since 1.0.0 + */ + private static function parseUrl(&$rawUrl, bool $relativeUrl): object + { + // Set up the return object + $result = new stdClass(); + $result->error = false; + $result->relative = $relativeUrl; + + // Handle relative URLs + $url = $rawUrl; + + if ($relativeUrl) { + $uri = Uri::getInstance()->base(true); + $url = $uri . "/" . $rawUrl; + } + + // Thanks to https://www.roscripts.com/PHP_regular_expressions_examples-136.html + // For parts of the URL regular expression here + if (preg_match( + '^(?P\b[A-Z]+\b://)?' + . '(?P[-A-Z0-9\.]+)?' + . ':?(?P[0-9]*)' + . '(?P/[-A-Z0-9+&@#/%=~_|!:,.;]*)' + . '?(?P\?[-A-Z0-9+&@#/%=~_|!:,.;]*)?^i', + $url, + $match + )) { + // Get the protocol (if any) + $protocol = ''; + + if (isset($match['protocol']) && $match['protocol']) { + $protocol = StringHelper::rtrim($match['protocol'], '/:'); + } + + // Get the domain (if any) + $domain = ''; + + if (isset($match['domain']) && $match['domain']) { + $domain = $match['domain']; + } + + // Figure out the port + $port = null; + + if ($protocol == 'http') { + $port = 80; + } elseif ($protocol == 'https') { + $port = 443; + } elseif ($protocol == 'ftp') { + $port = 21; + } elseif ($protocol == '') { + $port = 80; + } else { + // Unrecognized protocol + $result->error = true; + $result->errorCode = 'url_unknown_protocol'; + $result->errorMsg = Text::sprintf('ATTACH_ERROR_UNKNOWN_PROTCOL_S_IN_URL_S', $protocol, $rawUrl); + + return $result; + } + + // Override the port if specified + if (isset($match['port']) && $match['port']) { + $port = (int)$match['port']; + } + + // Default to HTTP if protocol/port is missing + if (!$port) { + $port = 80; + } + + // Get the path and reconstruct the full path + if (isset($match['path']) && $match['path']) { + $path = $match['path']; + } else { + $path = '/'; + } + + // Get the parameters (if any) + if (isset($match['parameters']) && $match['parameters']) { + $parameters = $match['parameters']; + } else { + $parameters = ''; + } + + // Handle relative URLs (or missing info) + if (!$relativeUrl) { + // If it is not a relative URL, make sure we have a protocl and domain + if ($protocol == '') { + $protocol = 'http'; + } + + if ($domain == '') { + // Reject bad url syntax + $result->error = true; + $result->errorCode = 'url_no_domain'; + $result->errorMsg = Text::sprintf('ATTACH_ERROR_IN_URL_SYNTAX_S', $rawUrl); + } + } + + // Save the information + $result->protocol = $protocol; + $result->domain = $domain; + $result->port = $port; + $result->path = str_replace('//', '/', $path); + $result->params = $parameters; + $result->url = str_replace('//', '/', $path . $result->params); + } else { + // Reject bad url syntax + $result->error = true; + $result->errorCode = 'url_bad_syntax'; + $result->errorMsg = Text::sprintf('ATTACH_ERROR_IN_URL_SYNTAX_S', $rawUrl); + } + + return $result; + } + + /** + * Get the info about this URL + * + * @access public + * + * @param string $rawUrl the raw url to parse + * @param &object $attachment the attachment object + * @param bool $verify whether the existance of the URL should be checked + * @param bool $relativeUrl allow relative URLs + * + * @return bool|object true if the URL is okay, or an error object if not + * + * @throws Exception + * + * @since 1.0.0 + */ + public static function getUrlInfo(string $rawUrl, &$attachment, bool $verify, bool $relativeUrl) + { + /* + Check the URL for existence + Get 'size' (null if the there were errors accessing the link, + or 0 if the URL loaded but had None/Null/0 for length + Get 'file_type' + Get 'filename' (for display) + */ + $u = self::parseUrl($rawUrl, $relativeUrl); + + // Deal with parsing errors + if ($u->error) { + return $u; + } + + // Set up defaults for what we want to know + $filename = basename($u->path); + $fileSize = 0; + $mimeType = ''; + $found = false; + + // Set the defaults + $attachment->filename = StringHelper::trim($filename); + $attachment->file_size = $fileSize; + $attachment->url_valid = false; + + // Get parameters + $params = ComponentHelper::getParams('com_attachments'); + $overlay = $params->get('superimpose_url_link_icons', true); + + // Get the timeout + $timeout = $params->get('link_check_timeout', 10); + + if (is_numeric($timeout)) { + $timeout = (int)$timeout; + } else { + $timeout = 10; + } + + // Check if fsockopen function is enabled + if (!function_exists('fsockopen')) { + return false; + } + + // Check the URL to see if it is valid + $errstr = null; + $fp = false; + + $app = Factory::getApplication(); + + if ($timeout > 0) { + // Set up error handler in case it times out or some other error occurs + + // For PHP +7.2 create_function rise deprecated error + if (version_compare(phpversion(), '7.2', 'ge')) { + set_error_handler( + function ($severity, $message, $file, $line) { + throw new Exception("fsockopen error"); + }, + E_ALL + ); + } else { + set_error_handler( + create_function('$severity, $message, $file, $line', 'throw new \Exception("fsockopen error");'), + E_ALL + ); + } + + // Https require diferent approach + $protocol = ""; + + if ($u->port == 443) { + $protocol = "ssl://"; + } + + try { + $fp = fsockopen($protocol . $u->domain, $u->port, $errno, $errstr, $timeout); + restore_error_handler(); + } catch (Exception $e) { + restore_error_handler(); + + if ($verify) { + $u->error = true; + $u->errorCode = 'url_check_exception'; + $u->errorMsg = $e->getMessage(); + } + } + + if ($u->error) { + $errorMsg = Text::sprintf('ATTACH_ERROR_CHECKING_URL_S', $rawUrl); + + if ($app->isClient('administrator')) { + $result = new stdClass(); + $result->error = true; + $result->errorMsg = $errorMsg; + + return $result; + } + + $u->errorMsg = $errorMsg; + + return $u; + } + } + + // Check the URL to get the size, etc + if ($fp) { + $request + = "HEAD $u->url HTTP/1.1\r\nHOST: $u->domain\r\n" + . "Connection: close\r\n" + . "User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:8.0.1) Gecko/20100101 Firefox/8.0.1\r\n" + . "Content-Type: application/x-www-form-urlencoded\r\n\r\n"; + + fputs($fp, $request); + + while (!feof($fp)) { + $httpResponse = fgets($fp, 128); + + // Check to see if it was found + if (preg_match("|^HTTP/1\.\d [0-9]+ ([^$]+)$|m", $httpResponse, $match)) { + if (trim($match[1]) == 'OK') { + $found = true; + } + } + + // Check for length + if (preg_match("/Content\-Length: (\d+)/i", $httpResponse, $match)) { + $fileSize = (int)$match[1]; + } + + // Check for content type + if (preg_match("/Content\-Type: ([^;$]+)/i", $httpResponse, $match)) { + $mimeType = trim($match[1]); + } + } + + fclose($fp); + + // Return error if it was not found (timed out, etc.) + if (!$found && $verify) { + $u->error = true; + $u->errorCode = 'url_not_found'; + $u->errorMsg = Text::sprintf('ATTACH_ERROR_COULD_NOT_ACCESS_URL_S', $rawUrl); + + return $u; + } + } else { + if ($verify && $timeout > 0) { + // Error connecting + $u->error = true; + $u->errorCode = 'url_error_connecting'; + $errorMsg = Text::sprintf('ATTACH_ERROR_CONNECTING_TO_URL_S', $rawUrl) . "
(" . $errstr . ")"; + $u->errorMsg = $errorMsg; + + return $u; + } + + if ($timeout == 0) { + // Pretend it was found + $found = true; + + if ($overlay) { + $mimeType = 'link/generic'; + } else { + $mimeType = 'link/unknown'; + } + } + } + + // Update the record + $attachment->filename = StringHelper::trim($filename); + $attachment->file_size = $fileSize; + $attachment->url_valid = (int)$found; + + // Deal with the file type + if (!$mimeType) { + $mimeType = AttachmentsFileTypes::mimeType($filename); + } + + if ($mimeType) { + $attachment->file_type = StringHelper::trim($mimeType); + } else { + if ($overlay) { + $mimeType = 'link/generic'; + $attachment->file_type = 'link/generic'; + } else { + $mimeType = 'link/unknown'; + $attachment->file_type = 'link/unknown'; + } + } + + // See if we can figure out the icon + $iconFilename = AttachmentsFileTypes::iconFilename($filename, $mimeType); + + if ($iconFilename) { + $attachment->icon_filename = $iconFilename; + } else { + if ($mimeType == 'link/unknown') { + $attachment->icon_filename = 'link.gif'; + } elseif ($mimeType == 'link/broken') { + $attachment->icon_filename = 'link_broken.gif'; + } else { + $attachment->icon_filename = 'link.gif'; + } + } + + return true; + } + +} diff --git a/attachments_component/admin/src/Model/AttachmentModel.php b/attachments_component/admin/src/Model/AttachmentModel.php index 5041e414..6daa39a3 100644 --- a/attachments_component/admin/src/Model/AttachmentModel.php +++ b/attachments_component/admin/src/Model/AttachmentModel.php @@ -15,17 +15,21 @@ namespace JMCameron\Component\Attachments\Administrator\Model; use JMCameron\Plugin\AttachmentsPluginFramework\AttachmentsPluginManager; +use JMCameron\Component\Attachments\Administrator\Helper\AttachmentsUploadHelper; +use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\MVC\Model\AdminModel; use Joomla\CMS\Plugin\PluginHelper; use Joomla\CMS\Table\Table; +use Joomla\CMS\Uri\Uri; +use Joomla\Database\ParameterType; +use Exception; // phpcs:disable PSR1.Files.SideEffects \defined('_JEXEC') or die; // phpcs:enable PSR1.Files.SideEffects - /** * Attachment Model * @@ -61,48 +65,174 @@ public function getTable($type = 'Attachment', $prefix = 'Administrator', $confi */ public function getItem($pk = null) { - $item = parent::getItem($pk); - - if ($item->id != 0) { - // If the item exists, get more info - /** @var \Joomla\Database\DatabaseDriver $db */ - $db = Factory::getContainer()->get('DatabaseDriver'); - - // Get the creator name - $query = $db->getQuery(true); - $query->select('name')->from('#__users')->where('id = ' . (int)$item->created_by); - try { - $db->setQuery($query, 0, 1); - $item->creator_name = $db->loadResult(); - } catch (\RuntimeException $e) { - $errmsg = $e->getMessage() . ' (ERR 112)'; - throw new \Exception($errmsg, 500); - } + if ($this->item = parent::getItem($pk)) + { + if ((int) $this->item->id > 0) + { + $db = Factory::getContainer()->get('DatabaseDriver'); + + $query = $db + ->getQuery(true) + ->select($db->qn('name')) + ->from($db->qn('#__users')) + ->where($db->qn('id') . ' = :id') + ->bind(':id', $this->item->created_by, ParameterType::INTEGER); + + try + { + $this->item->author_name = $db + ->setQuery($query, 0, 1) + ->loadResult(); + } + catch (Exception $e) + { + Factory::getApplication()->enqueueMessage($e->getMessage() . ' (ERR 112)', 'error'); + } + + $query = $db + ->getQuery(true) + ->select($db->qn('name')) + ->from($db->qn('#__users')) + ->where($db->qn('id') . ' = :id') + ->bind(':id', $this->item->modified_by, ParameterType::INTEGER); + + try + { + $this->item->editor_name = $db + ->setQuery($query, 0, 1) + ->loadResult(); + } + catch (Exception $e) + { + Factory::getApplication()->enqueueMessage($e->getMessage() . ' (ERR 113)', 'error'); + } + + $uri = Uri::getInstance(); + /* problem z generowaniem kolejnych / w urlu + // Fix the URL for files + if ($this->item->uri_type == 'file') + { + $this->item->url = $uri->root(true) . '/' . $this->item->url; + } + */ + $parentId = $this->item->parent_id; + $parentType = $this->item->parent_type; + $parentEntity = $this->item->parent_entity; + + // Get the parent handler + PluginHelper::importPlugin('attachments'); + $apm = AttachmentsPluginManager::getAttachmentsPluginManager(); + + if (!$apm->attachmentsPluginInstalled($parentType)) + { + // Exit if there is no Attachments plugin to handle this parentType + $errMsg = Text::sprintf('ATTACH_ERROR_INVALID_PARENT_TYPE_S', $parentType) . ' (ERR 133)'; + Factory::getApplication()->enqueueMessage($errMsg, 'error'); - // Get the modifier name - $query = $db->getQuery(true); - $query->select('name')->from('#__users')->where('id = ' . (int)$item->modified_by); - try { - $db->setQuery($query, 0, 1); - $item->modifier_name = $db->loadResult(); - } catch (\RuntimeException $e) { - $errmsg = $e->getMessage() . ' (ERR 113)'; - throw new \Exception($errmsg, 500); + return false; + } + + $entityInfo = $apm->getInstalledEntityInfo(); + $parent = $apm->getAttachmentsPlugin($parentType); + + // Get the parent info + $parentEntityName = Text::_('ATTACH_' . $parentEntity); + $parentTitle = $parent->getTitle($parentId, $parentEntity); + + if (!$parentTitle) + { + $parentTitle = Text::sprintf('ATTACH_NO_PARENT_S', $parentEntityName); + } + + $this->item->parent_id = $parentId; + $this->item->parent_entity_name = $parentEntityName; + $this->item->parent_title = $parentTitle; + $this->item->parent_published = $parent->isParentPublished($parentId, $parentEntity); + + // Set parent type and entity field accordingly, also set id as parenttype__entitytype = parent_id field + $this->item->parent_type_list = $this->item->parent_type . '__' . $this->item->parent_entity; + $this->item->{$parentType . '__' . $parentEntity} = $this->item->parent_id; } + else + { + $app = Factory::getApplication(); + $gus = $app->getUserState('com_attachments.edit.attachment.data'); - // Get the parent info (??? Do we really need this?) - $parent_type = $item->parent_type; - $parent_entity = $item->parent_entity; - PluginHelper::importPlugin('attachments'); - $apm = AttachmentsPluginManager::getAttachmentsPluginManager(); - if (!$apm->attachmentsPluginInstalled($parent_type)) { - $errmsg = Text::sprintf('ATTACH_ERROR_INVALID_PARENT_TYPE_S', $parent_type) . ' (ERR 114)'; - throw new \Exception($errmsg, 500); + $input = $app->getInput(); + $editor = $input->getString('editor', ''); + + if ($gus == null) + { + // Get some info from previous link (ie from xtd-button, addAttachment button) + $from = $input->getString('from', ''); + $editor = $input->getString('editor', ''); + $parentType = explode('.', $input->getString('parent_type', 'com_content.article')); + $parentId = $input->getInt('parent_id', 0); + } + else + { + // Get some info from previously set form data + $from = $gus['from']; + $parentType = explode('__', $gus['parent_type_list']); + $parentId = $gus['parent_id']; + } + + // Set up the "select parent" controls + PluginHelper::importPlugin('attachments'); + $apm = AttachmentsPluginManager::getAttachmentsPluginManager(); + + if (!$apm->attachmentsPluginInstalled($parentType[0])) + { + // Exit if there is no Attachments plugin to handle this parentType + $errMsg = Text::sprintf('ATTACH_ERROR_INVALID_PARENT_TYPE_S', $parentType[0]) . ' (ERR 123)'; + Factory::getApplication()->enqueueMessage($errMsg, 'error'); + } + + $entityInfo = $apm->getInstalledEntityInfo(); + $parent = $apm->getAttachmentsPlugin($parentType[0]); + + $parentEntity = $parent->getCanonicalEntityId($parentType[1]); + $parentEntityName = Text::_('ATTACH_' . $parentEntity); + + // Disable the main menu items + $input->get('hidemainmenu', 1); + + // Get the parent entity title + $parentTitle = ''; + + if ((int) $parentId > 0) + { + $parentTitle = $parent->getTitle($parentId, $parentEntity); + } + + // Get the component parameters + $params = ComponentHelper::getParams('com_attachments'); + + // We do not have a real attachment yet so fake it + $this->item = new \stdClass(); + $this->item->id = 0; + $this->item->uri_type = 'file'; + $this->item->state = $params->get('publish_default', false); + $this->item->url = ''; + $this->item->url_relative = false; + $this->item->url_verify = true; + $this->item->display_name = ''; + $this->item->description = ''; + $this->item->user_field_1 = ''; + $this->item->user_field_2 = ''; + $this->item->user_field_3 = ''; + $this->item->parent_id = $parentId; + $this->item->parent_type = $parentType[0]; + $this->item->parent_entity = $parentEntity; + $this->item->parent_title = $parentTitle; + $this->item->parent_type_list = $parentType[0] . '__' . $parentEntity; + $this->item->{$parentType[0] . '__' . $parentEntity} = $parentId; + $this->item->editor = $editor; + $this->item->from = $from; } - $item->parent = $apm->getAttachmentsPlugin($parent_type); } - return $item; + return $this->item; } /** @@ -124,6 +254,25 @@ public function getForm($data = array(), $loadData = true) if (empty($form)) { return false; } + + /** + * TODO FOR FURTHER RELEASES: Add new parent types + * Here we create additional fields for new parent types entities - we need to create a way to gather info about new forms of parents + * Test against showon parameter for new parents, showon should be set in field->setAttribute method + * We need to remember to add new parents to attachments filter on main form + * Possible code for adding new parents to select list: + * $form->getField("parent_type_list")->addOption("Another type", ['value' => 'com_xyz__1']); + * $form->getField("parent_type_list")->addOption("Yet another type", ['value' => 'com_xyz__2']); + * $fields = []; + * $fields[] = ''; + * $fields[] = ''; + * $element = '
' . implode('', $fields) . '
'; + * $xml = new SimpleXMLElement($element); + * $form->setField($xml, null, true, 'additionalcontentfields'); + * + * Code should also be set in site AttachmentModel getForm. + */ + return $form; } @@ -145,4 +294,260 @@ protected function loadFormData() } return $data; } + + /** + * Validate parent info, type, entity, id + * + * @access protected + * + * @param &object $data attachment data to validate + * @param &object $parent parent info to which we validate + * + * @return boolean true if success + * + * @throws Exception + * + * @since 4.2 + */ + protected function validateParent(&$data, &$parent): bool + { + // Test parent selection + if ($data->parent_type_list) + { + $parent = explode('__', $data->parent_type_list); + + if (count($parent) != 2) + { + $this->setError(Text::sprintf('ATTACH_ERROR_INVALID_PARENT_TYPE_S', $parent[0])); + + return false; + } + + $data->parent_type = $parent[0]; + + if ($data->parent_type == 'com_categories') + { + $data->parent_type = 'com_content'; + } + + $data->parent_entity = $parent[1]; + $data->parent_id = $data->{$data->parent_type_list}; + + // todo rozpoznawac new content ktory nie ma jeszcze id a mamy juz dodany zalacznik + if (empty($data->parent_id)) + { + $this->setError(Text::sprintf('ATTACH_ERROR_INVALID_PARENT_ENTITY_NOT_SELECTED', $data->parent_id) . ' (ERR 122)'); + + return false; + } + + PluginHelper::importPlugin('attachments'); + $apm = AttachmentsPluginManager::getAttachmentsPluginManager();; + + $entityInfo = $apm->getInstalledEntityInfo(); + $parent = $apm->getAttachmentsPlugin($data->parent_type); + + if (empty($parent)) + { + $this->setError(Text::sprintf('ATTACH_ERROR_INVALID_PARENT_TYPE_S', $data->parent_type)); + + return false; + } + + $parent->id = $data->parent_id; + $parent->type = $data->parent_type; + $parent->entity = $parent->getCanonicalEntityName($data->parent_entity); + // TODO: odkomentowanie tego generuje blad abstract!!! zweryfikowac uzytecznosc tego - nie zamyka sie modalny przez to ?? +// $parent->entity_name = Text::_('ATTACH_' . $data->parent_entity); + } + else + { + $this->setError(Text::sprintf('ATTACH_ERROR_INVALID_PARENT_ID_S', $data->parent_id) . ' (ERR 123)'); + + return false; + } + + return true; + } + + /** + * Validate URI info if url attachment type was selected + * + * @access protected + * + * @param &object $data attachment data to validate + * + * @return boolean true if success + * + * @since 4.1.5 + */ + protected function validateUriType(&$data): bool + { + + if ($data->uri_type === 'url') + { + if (empty((string) $data->url)) + { + $this->setError(Text::sprintf('ATTACH_ERROR_IN_URL_EMPTY_S') . ' (ERR 124)'); + + if (filter_var($data->url, FILTER_VALIDATE_URL) === false) + { + $this->setError(Text::sprintf('ATTACH_ERROR_IN_URL_INVALID_S') . ' (ERR 125)'); + } + + return false; + } + } + + return true; + } + + /** + * Validate URI info if url attachment type was selected + * + * @access protected + * + * @param &object $data attachment data to validate + * @param &object $parent parent info to which we validate + * @param &object $table attachment data from table + * + * @return boolean true if success + * + * @throws Exception + * + * @since 4.1.5 + */ + protected function validateUploadFile(&$data, &$parent, &$table): bool + { + if ((string) $data->uri_type == 'file') + { + // Get the component parameters + $params = ComponentHelper::getParams('com_attachments'); + $testSafeFile = $params->get('test_is_safe_file', true); + + if ($testSafeFile) + { + $ignoreIsSafeTest = 'CMD'; + } + else + { + $ignoreIsSafeTest = 'RAW'; + } + + $files = Factory::getApplication()->input->files->get('jform', -9, $ignoreIsSafeTest); + + // file previously uploaded and not changed + if (($files['filename']['error'] == 4) && (!empty($data->filename_sys))) { + return true; + } + + if (((int) $files !== -9) && ($files['filename']['error'] && ($files['filename']['error'] == UPLOAD_ERR_NO_FILE))) + { + // Filename was not provided - update only other fields + if ($data->id) + { + // Move attachment file system location when parent was changed + $errMsg = AttachmentsUploadHelper::switchParent($data, $data->parent_id, $data->parent_type, $data->parent_entity); + + if ($errMsg) + { + throw new Exception($errMsg, 500); + } + + $table->filename_sys = $data->filename_sys; + $table->url = $data->url; + + return $table->store(); + } + else + { + $this->setError(Text::_('ATTACH_YOU_MUST_SELECT_A_FILE_TO_UPLOAD')); + + return false; + } + } + else + { + if (((int) $files === -9) && ($ignoreIsSafeTest !== 'RAW')) + { + $this->setError(Text::_('ATTACH_ERROR_FILENAME_INSECURE_S') . ' (ERR 126)'); + + return false; + } + + $errMsg = AttachmentsUploadHelper::uploadFile($table, $parent, $files, $data->id == 0); + + if (!empty($errMsg)) + { + $this->setError($errMsg); + } + + return $errMsg == ''; + } + } + + return true; + } + /** + * Method to save the form data. + * + * @access public + * + * @param array $data The form data. + * + * @return boolean True on success. + * + * @throws Exception + * + * @since 4.2 + */ + public function save($data): bool + { + $this->parent = null; + $data = (object) $data; + $table = $this->getTable(); + + $id = Factory::getApplication()->input->getInt('id'); + + if (!$this->validateParent($data, $this->parent)) + { + return false; + } + + if (!$this->validateUriType($data)) + { + return false; + } + + $table->load($id); + + // Prepare info for possible parent type switch - move attachment to new location + $props = $table->getProperties(); + $data->parent_id_old = $props['parent_id']; + $data->parent_type_old = $props['parent_type']; + $data->parent_entity_old = $props['parent_entity']; + $data->filename = $props['filename']; + $data->filename_sys = $props['filename_sys']; + + if (!$table->bind($data)) + { + return false; + } + + if ($data->uri_type === 'url') + { + return $table->store(); + } + else + { + if (!$this->validateUploadFile($data, $this->parent, $table)) + { + return false; + } + } + + $this->cleanCache(); + + return true; + } } diff --git a/attachments_component/admin/src/Table/AttachmentTable.php b/attachments_component/admin/src/Table/AttachmentTable.php index 7a174723..ee24c2de 100644 --- a/attachments_component/admin/src/Table/AttachmentTable.php +++ b/attachments_component/admin/src/Table/AttachmentTable.php @@ -211,6 +211,34 @@ public function store($updateNulls = false) $this->url_valid = $this->url_valid ? 1 : 0; $this->url_relative = $this->url_relative ? 1 : 0; $this->url_verify = $this->url_verify ? 1 : 0; + if ($this->uri_type === 'url') + { + $this->filename = ''; + $this->filename_sys = ''; + $this->file_type = 'unknown'; + $this->file_size = 0; + $this->icon_filename = 'link.gif'; + } + + // Set the create/modify dates + $now = Factory::getDate(); + $now = $now->toSql(); + + $userId = Factory::getApplication()->getIdentity()->id; + + if (empty($this->created)) + { + $this->created = $now; + } + + $this->modified = $now; + + if (empty($this->created_by)) + { + $this->created_by = $userId; + } + + $this->modified_by = $userId; // Let the parent class do the real work! return parent::store($updateNulls); diff --git a/attachments_component/admin/tmpl/attachment/edit.php b/attachments_component/admin/tmpl/attachment/edit.php new file mode 100644 index 00000000..8c07503f --- /dev/null +++ b/attachments_component/admin/tmpl/attachment/edit.php @@ -0,0 +1,219 @@ +getDocument(); +$input = $app->input; +$uri = Uri::getInstance(); + +// In case of modal +$isModal = $input->get('layout') === 'modal'; +$layout = $isModal ? 'modal' : 'edit'; +$tmpl = $isModal || $input->get('tmpl') === 'component' ? '&tmpl=component' : ''; +$editor = $input->get('editor'); + +$wa = $app->getDocument()->getWebAssetManager(); +$wa->useScript('keepalive') + ->useScript('form.validate'); +?> + +
+ +
+ 'general', 'recall' => true, 'breakpoint' => 768] + ); ?> + item->parent_title ) { + // echo "

" . Text::sprintf('ATTACH_PARENT_S_COLON_S', $this->item->parent_entity_name, $this->item->parent_title) . "

"; + // } + ?> +
+
+
+ form->renderField('parent_type_list'); ?> + form->getFieldset('content_types') as $field) { + echo $field->renderField(); + } + ?> + form->renderField('uri_type'); ?> + item->uri_type == 'file'): + if ($this->item->id != 0) { + echo $this->form->renderField('filename_current'); + } ?> + + + form->renderField('filename'); ?> + form->renderField('url'); ?> + form->renderField('url_note'); ?> +
+
+ form->renderField('url_valid'); ?> +
+
+ form->renderField('url_relative'); ?> +
+
+
+
+ form->renderField('display_name'); ?> +
+
+ +
+
+ form->renderField('description'); ?> + may_publish ): ?> +
+
+
+ form->renderField('state'); ?> +
+
+
+
+ + form->renderField('access'); ?> +
+
+
+ show_user_field_1): ?> + form->renderField('user_field_1'); ?> + + show_user_field_2): ?> + form->renderField('user_field_2'); ?> + + show_user_field_3): ?> + form->renderField('user_field_3'); ?> + + + item->parent_id == 0): ?> + + item->parent_id): ?> + + + + + get('parent_type'),); + // todo przerobic ponizsze hidden na renderfield->hidden ?? jesli sie da + ?> + + + + + + + + item->from == 'closeme'): ?> +
+ + + + +
+ + +
+
+
+ + +
+
+item->uri_type == 'file') && $this->item->parent_id) { + /** Get the Attachments controller class */ + /** @var \Joomla\CMS\MVC\Factory\MVCFactory $mvc */ + $mvc = Factory::getApplication() + ->bootComponent("com_attachments") + ->getMVCFactory(); + /** @var \JMCameron\Component\Attachments\Administrator\Controller\ListController $controller */ + $controller = $mvc->createController('List', 'Administrator', [], $app, $app->getInput()); +// $controller->displayString($this->item->parent_id, $this->item->parent_type, $this->item->parent_entity, +// 'ATTACH_EXISTING_ATTACHMENTS', false, false, true, $this->from); +} diff --git a/attachments_plugin_framework/src/PlgAttachmentsFramework.php b/attachments_plugin_framework/src/PlgAttachmentsFramework.php index 7608c4fa..0b42a313 100644 --- a/attachments_plugin_framework/src/PlgAttachmentsFramework.php +++ b/attachments_plugin_framework/src/PlgAttachmentsFramework.php @@ -330,6 +330,49 @@ public function getCanonicalEntityId($parent_entity) } } + /** + * Get the canonical extension entity Name (eg, 'article' instead of 'default') + * + * This is the canonical Id of content element/item to which attachments will be added. + * + * Note that each content type ($option) may support several different entities + * (for attachments) and some entities may have more than one name. + * + * Note, for com_content, the default is 'article' + * + * @access public + * + * @param string $parentEntity the type of entity for this parent type + * + * @return string the canonical extension entity + * + * @throws Exception + * + * @since 4.2.0 + */ + public function getCanonicalEntityName(string $parentEntity): string + { + // If it is a known entity, just return it + if (is_array($this->entities) && in_array($parentEntity, $this->entities)) + { + return $parentEntity; + } + + // Check aliases + if (is_array($this->entities) && array_key_exists($parentEntity, $this->entity_name)) + { + return $this->entity_name[$parentEntity]; + } + else + { + $errMsg = Text::sprintf('PLG_ATTACHMENTS_PLUGIN_ERROR_INVALID_ENTITY_S_FOR_PARENT_S', $parentEntity, $this->parentType) . ' (ERR 300)'; + + $this->app->enqueueMessage($errMsg, 'error'); + } + + return ''; + } + /** * Get the path for the uploaded file (on the server file system) *