diff --git a/administrator/components/com_admin/sql/updates/mysql/4.1.0-2021-08-26.sql b/administrator/components/com_admin/sql/updates/mysql/4.1.0-2021-08-26.sql new file mode 100644 index 0000000000000..8bc9f1e200233 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/mysql/4.1.0-2021-08-26.sql @@ -0,0 +1,12 @@ +-- after 4.0.0 RC1 + CREATE TABLE IF NOT EXISTS `#__draft` ( + `article_id` int unsigned NOT NULL, + `version_id` int unsigned NOT NULL, + `state` tinyint NOT NULL DEFAULT '0', + `hashval` varchar(2083) NOT NULL DEFAULT '', + `shared_date` datetime DEFAULT NULL, + PRIMARY KEY(`article_id`, `version_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; + +ALTER TABLE `#__content` ADD COLUMN `shared` tinyint(3) UNSIGNED NOT NULL DEFAULT '0'; +ALTER TABLE `#__content` ADD COLUMN `draft` tinyint(3) UNSIGNED NOT NULL DEFAULT '0'; \ No newline at end of file diff --git a/administrator/components/com_admin/sql/updates/postgresql/4.1.0-2021-08-26.sql b/administrator/components/com_admin/sql/updates/postgresql/4.1.0-2021-08-26.sql new file mode 100644 index 0000000000000..de15783ceadf6 --- /dev/null +++ b/administrator/components/com_admin/sql/updates/postgresql/4.1.0-2021-08-26.sql @@ -0,0 +1,13 @@ +-- after 4.0.0 RC1 + -- SQLINES LICENSE FOR EVALUATION USE ONLY + CREATE TABLE IF NOT EXISTS "#__draft" ( + article_id int check (article_id > 0) NOT NULL, + version_id int check (version_id > 0) NOT NULL, + state smallint NOT NULL DEFAULT '0', + hashval varchar(2083) NOT NULL DEFAULT '', + shared_date timestamp(0) DEFAULT NULL, + PRIMARY KEY(article_id, version_id) +) ; + +ALTER TABLE "#__content" ADD COLUMN shared tinyint(3) UNSIGNED NOT NULL DEFAULT '0'; +ALTER TABLE "#__content" ADD COLUMN draft tinyint(3) UNSIGNED NOT NULL DEFAULT '0'; \ No newline at end of file diff --git a/administrator/components/com_banners/forms/banner.xml b/administrator/components/com_banners/forms/banner.xml index 87320231396a9..57a19fa785b68 100644 --- a/administrator/components/com_banners/forms/banner.xml +++ b/administrator/components/com_banners/forms/banner.xml @@ -52,6 +52,7 @@ + JUNPUBLISHED + JUNPUBLISHED + JUNPUBLISHED + diff --git a/administrator/components/com_content/config.xml b/administrator/components/com_content/config.xml index 1362527a3349e..68c792b145871 100644 --- a/administrator/components/com_content/config.xml +++ b/administrator/components/com_content/config.xml @@ -942,6 +942,8 @@ + + diff --git a/administrator/components/com_content/forms/article.xml b/administrator/components/com_content/forms/article.xml index 3b6ad8bcd589d..ae603acc4effa 100644 --- a/administrator/components/com_content/forms/article.xml +++ b/administrator/components/com_content/forms/article.xml @@ -79,7 +79,9 @@ + + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + +
+ +
+ + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+ +
+ + + + + + + + + + + + + + + + + + + +
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+ + +
+ + + + + + + + + + +
+ +
+ + + + + + + + + +
+
+ + +
+ + + + + + + + + + +
+ +
+ + + + + + + + + + +
+ +
+ + + + + + + + + + +
+
+ + +
+ + + + + + + + + +
+
+ + + + + + + +
diff --git a/administrator/components/com_content/forms/filter_articles.xml b/administrator/components/com_content/forms/filter_articles.xml index 73c1bb84e6f34..6eea0efcc394d 100644 --- a/administrator/components/com_content/forms/filter_articles.xml +++ b/administrator/components/com_content/forms/filter_articles.xml @@ -133,6 +133,8 @@ + + diff --git a/administrator/components/com_content/forms/filter_drafts.xml b/administrator/components/com_content/forms/filter_drafts.xml new file mode 100644 index 0000000000000..73c1bb84e6f34 --- /dev/null +++ b/administrator/components/com_content/forms/filter_drafts.xml @@ -0,0 +1,170 @@ + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
diff --git a/administrator/components/com_content/src/Controller/ArticleController.php b/administrator/components/com_content/src/Controller/ArticleController.php index 27a76274978ba..ee0cd96c7f9e3 100644 --- a/administrator/components/com_content/src/Controller/ArticleController.php +++ b/administrator/components/com_content/src/Controller/ArticleController.php @@ -1,4 +1,5 @@ app->setUserState('com_menus.edit.item', array( - 'data' => $editState, - 'type' => $type, - 'link' => $link) + $this->app->setUserState( + 'com_menus.edit.item', + array( + 'data' => $editState, + 'type' => $type, + 'link' => $link + ) ); $this->setRedirect(Route::_('index.php?option=com_menus&view=item&client_id=0&menutype=mainmenu&layout=edit', false)); @@ -182,4 +188,33 @@ public function batch($model = null) return parent::batch($model); } + + public function saveAsDraft() + { + $this->checkToken(); + + $user = $this->app->getIdentity(); + $id = $this->input->get('id', 0, 'integer'); + + // $id = $this->input->data->id; + + $redirectUrl = 'index.php?option=com_content&view=' . $this->view_list . $this->getRedirectToListAppend(); + + // Access checks. + if (!$user->authorise('core.edit.state', 'com_content.article.' . (int) $id)) + { + // Prune items that you can't change. + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); + } + + // Get the model. + /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ + $model = $this->getModel(); + + $model->storeHistory($id); + $model->storeDraft($id); + + $message = Text::plural('COM_CONTENT_N_ITEMS_DRAFTED', 1); + $this->setRedirect(Route::_($redirectUrl, false), $message); + } } diff --git a/administrator/components/com_content/src/Controller/ArticlesController.php b/administrator/components/com_content/src/Controller/ArticlesController.php index a037239355c1e..cda9197886a4f 100644 --- a/administrator/components/com_content/src/Controller/ArticlesController.php +++ b/administrator/components/com_content/src/Controller/ArticlesController.php @@ -1,4 +1,5 @@ checkToken(); + + $user = $this->app->getIdentity(); + $ids = $this->input->get('cid', array(), 'array'); + $redirectUrl = 'index.php?option=com_content&view=' . $this->view_list . $this->getRedirectToListAppend(); + $message = ''; + + // Access checks. + foreach ($ids as $i => $id) + { + if (!$user->authorise('core.edit.state', 'com_content.article.' . (int) $id)) + { + // Prune items that you can't change. + unset($ids[$i]); + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); + } + } + + if (empty($ids)) + { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); + } + else + { + // Get the model. + /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ + /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ + $model = $this->getModel(); + + foreach ($ids as $id) + { + $model->storeHistory($id); + $model->storeDraft($id); + } + + $message = Text::plural('COM_CONTENT_N_ITEMS_DRAFTED', count($ids)); + } + + $message = Text::plural('COM_CONTENT_N_ITEMS_DRAFTED', count($ids)); + $this->setRedirect(Route::_($redirectUrl, false), $message); + } } diff --git a/administrator/components/com_content/src/Controller/DraftsController.php b/administrator/components/com_content/src/Controller/DraftsController.php new file mode 100644 index 0000000000000..3d46630e02b15 --- /dev/null +++ b/administrator/components/com_content/src/Controller/DraftsController.php @@ -0,0 +1,314 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Content\Administrator\Controller; + +\defined('_JEXEC') or die; + +use Joomla\CMS\Application\CMSApplication; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Controller\AdminController; +use Joomla\CMS\MVC\Factory\MVCFactoryInterface; +use Joomla\CMS\Response\JsonResponse; +use Joomla\CMS\Router\Route; +use Joomla\Input\Input; +use Joomla\Utilities\ArrayHelper; + +/** + * Drafts list controller class. + * + * @since 1.6 + */ +class DraftsController extends AdminController +{ + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * Recognized key values include 'name', 'default_task', 'model_path', and + * 'view_path' (this list is not meant to be comprehensive). + * @param MVCFactoryInterface $factory The factory. + * @param CMSApplication $app The JApplication for the dispatcher + * @param Input $input Input + * + * @since 3.0 + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, $app = null, $input = null) + { + parent::__construct($config, $factory, $app, $input); + + // Articles default form can come from the articles or featured view. + // Adjust the redirect view on the value of 'view' in the request. + if ($this->input->get('view') == 'featured') + { + $this->view_list = 'featured'; + } + + $this->registerTask('unfeatured', 'featured'); + } + + /** + * Method to toggle the featured setting of a list of articles. + * + * @return void + * + * @since 1.6 + */ + public function featured() + { + // Check for request forgeries + $this->checkToken(); + + $user = $this->app->getIdentity(); + $ids = $this->input->get('cid', array(), 'array'); + $values = array('featured' => 1, 'unfeatured' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + $redirectUrl = 'index.php?option=com_content&view=' . $this->view_list . $this->getRedirectToListAppend(); + + // Access checks. + foreach ($ids as $i => $id) + { + if (!$user->authorise('core.edit.state', 'com_content.article.' . (int) $id)) + { + // Prune items that you can't change. + unset($ids[$i]); + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); + } + } + + if (empty($ids)) + { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); + } + else + { + // Get the model. + /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ + $model = $this->getModel(); + + // Publish the items. + if (!$model->featured($ids, $value)) + { + $this->setRedirect(Route::_($redirectUrl, false), $model->getError(), 'error'); + + return; + } + + if ($value == 1) + { + $message = Text::plural('COM_CONTENT_N_ITEMS_FEATURED', count($ids)); + } + else + { + $message = Text::plural('COM_CONTENT_N_ITEMS_UNFEATURED', count($ids)); + } + } + + $this->setRedirect(Route::_($redirectUrl, false), $message); + } + + /** + * Proxy for getModel. + * + * @param string $name The model name. Optional. + * @param string $prefix The class prefix. Optional. + * @param array $config The array of possible config values. Optional. + * + * @return \Joomla\CMS\MVC\Model\BaseDatabaseModel + * + * @since 1.6 + */ + public function getModel($name = 'Draft', $prefix = 'Administrator', $config = array('ignore_request' => true)) + { + return parent::getModel($name, $prefix, $config); + } + + /** + * Method to get the number of published articles for quickicons + * + * @return string The JSON-encoded amount of published articles + * + * @since 4.0.0 + */ + public function getQuickiconContent() + { + $model = $this->getModel('drafts'); + + $model->setState('filter.published', 1); + + $amount = (int) $model->getTotal(); + + $result = []; + + $result['amount'] = $amount; + $result['sronly'] = Text::plural('COM_CONTENT_N_QUICKICON_SRONLY', $amount); + $result['name'] = Text::plural('COM_CONTENT_N_QUICKICON', $amount); + + echo new JsonResponse($result); + } + + /** + * Method to toggle the unsharing draft. + * + * @return void + * + * @since 1.6 + */ + public function unshareDrafts() + { + // Check for request forgeries + $this->checkToken(); + + // Get the input + $task = $this->getTask(); + $pks = $this->input->post->get('cid', array(), 'array'); + $value = 1; + $redirectUrl = 'index.php?option=com_content&view=' . $this->view_list; + $message = ''; + + /** @var \Joomla\Component\Content\Administrator\Model\DraftModel $model */ + $model = $this->getModel(); + + // Publish the items. + if (!$model->unshare($pks)) + { + $this->setRedirect(Route::_($redirectUrl, false), $model->getError(), 'error'); + + return; + } + + // TODO: HERE properly messages + $message = Text::plural('COM_CONTENT_N_ITEMS_UNSHARED', count($pks)); + + $this->setRedirect(Route::_($redirectUrl, false), $message); + } + + public function shareDrafts() + { + // Check for request forgeries + $this->checkToken(); + + // Get the input + $task = $this->getTask(); + $pks = $this->input->post->get('cid', array(), 'array'); + $value = 1; + $redirectUrl = 'index.php?option=com_content&view=' . $this->view_list; + $message = ''; + + /** @var \Joomla\Component\Content\Administrator\Model\DraftModel $model */ + $model = $this->getModel(); + + // Publish the items. + if (!$model->share($pks, $value)) + { + $this->setRedirect(Route::_($redirectUrl, false), $model->getError(), 'error'); + + return; + } + + // TODO: HERE properly messages + $message = Text::plural('COM_CONTENT_N_ITEMS_SHARED', count($pks)); + + $this->setRedirect(Route::_($redirectUrl, false), $message); + } + + + public function deleteDrafts() + { + // Check for request forgeries + $this->checkToken(); + + // Get the input + $pks = $this->input->post->get('cid', array(), 'array'); + $redirectUrl = 'index.php?option=com_content&view=' . $this->view_list; + $message = ''; + + /** @var \Joomla\Component\Content\Administrator\Model\DraftModel $model */ + $model = $this->getModel(); + + // Publish the items. + foreach ($pks as $id) + { + if (!$model->delete($id)) + { + $this->setRedirect(Route::_($redirectUrl, false), $model->getError(), 'error'); + + return; + } + } + + $message = Text::plural('COM_CONTENT_N_ITEMS_DELETED', count($pks)); + + $this->setRedirect(Route::_($redirectUrl, false), $message); + } + + + /** + * Method to toggle the featured setting of a list of articles. + * + * @return void + * + * @since 1.6 + */ + public function published() + { + // Check for request forgeries + $this->checkToken(); + + $user = $this->app->getIdentity(); + $ids = $this->input->get('cid', array(), 'array'); + $values = array('featured' => 1, 'unfeatured' => 0); + $task = $this->getTask(); + $value = ArrayHelper::getValue($values, $task, 0, 'int'); + $redirectUrl = 'index.php?option=com_content&view=' . $this->view_list . $this->getRedirectToListAppend(); + + // Access checks. + foreach ($ids as $i => $id) + { + if (!$user->authorise('core.edit.state', 'com_content.article.' . (int) $id)) + { + // Prune items that you can't change. + unset($ids[$i]); + $this->app->enqueueMessage(Text::_('JLIB_APPLICATION_ERROR_EDITSTATE_NOT_PERMITTED'), 'notice'); + } + } + + if (empty($ids)) + { + $this->app->enqueueMessage(Text::_('JERROR_NO_ITEMS_SELECTED'), 'error'); + } + else + { + // Get the model. + /** @var \Joomla\Component\Content\Administrator\Model\ArticleModel $model */ + $model = $this->getModel(); + + // Publish the items. + if (!$model->featured($ids, $value)) + { + $this->setRedirect(Route::_($redirectUrl, false), $model->getError(), 'error'); + + return; + } + + if ($value == 1) + { + $message = Text::plural('COM_CONTENT_N_ITEMS_FEATURED', count($ids)); + } + else + { + $message = Text::plural('COM_CONTENT_N_ITEMS_UNFEATURED', count($ids)); + } + } + + $this->setRedirect(Route::_($redirectUrl, false), $message); + } +} diff --git a/administrator/components/com_content/src/Extension/ContentComponent.php b/administrator/components/com_content/src/Extension/ContentComponent.php index 84780cc8ec041..9517e76aa6212 100644 --- a/administrator/components/com_content/src/Extension/ContentComponent.php +++ b/administrator/components/com_content/src/Extension/ContentComponent.php @@ -62,6 +62,7 @@ class ContentComponent extends MVCComponent implements /** * The trashed condition * + * * @since 4.0.0 */ const CONDITION_NAMES = [ @@ -69,6 +70,7 @@ class ContentComponent extends MVCComponent implements self::CONDITION_UNPUBLISHED => 'JUNPUBLISHED', self::CONDITION_ARCHIVED => 'JARCHIVED', self::CONDITION_TRASHED => 'JTRASHED', + self::CONDITION_DRAFTED => 'JDRAFTED', ]; /** @@ -99,6 +101,8 @@ class ContentComponent extends MVCComponent implements */ const CONDITION_TRASHED = -2; + const CONDITION_DRAFTED = -3; + /** * Booting the extension. This is the function to set up the environment of the extension like * registering new class loaders, etc. diff --git a/administrator/components/com_content/src/Model/ArticleModel.php b/administrator/components/com_content/src/Model/ArticleModel.php index bc9bd82c04633..e3cbcedbd2117 100644 --- a/administrator/components/com_content/src/Model/ArticleModel.php +++ b/administrator/components/com_content/src/Model/ArticleModel.php @@ -1,4 +1,5 @@ bind(':featuredUp', $featured->featured_up, $featured->featured_up ? ParameterType::STRING : ParameterType::NULL) ->bind(':featuredDown', $featured->featured_down, $featured->featured_down ? ParameterType::STRING : ParameterType::NULL); - $db->setQuery($query); - $db->execute(); + $db->setQuery($query); + $db->execute(); } } @@ -291,6 +292,99 @@ protected function batchMove($value, $pks, $contexts) return true; } + public function storeDraft($pk) + { + try + { + $db = $this->getDbo(); + $item_id = "com_content.article." . $pk; + + $query = $db->getQuery(true) + ->select( + [ + $db->quoteName('h.version_id') + ] + ) + ->from($db->quoteName('#__history', 'h')) + ->where($db->quoteName('item_id') . ' = :item_id') + ->bind(':item_id', $item_id, ParameterType::STRING) + ->order('version_id DESC')->setLimit(1); + + $version_id = $db->setQuery($query)->loadResult(); + $hashval = $version_id . uniqid(); + + $query = $db->getQuery(true) + ->insert($db->quoteName('#__draft')) + ->columns( + [ + $db->quoteName('article_id'), + $db->quoteName('version_id'), + $db->quoteName('hashval'), + ] + ) + ->values(':article_id, :version_id, :hashval') + ->bind(':article_id', $pk, ParameterType::INTEGER) + ->bind(':version_id', $version_id, ParameterType::INTEGER) + ->bind(':hashval', $hashval, ParameterType::STRING); + $db->setQuery($query)->execute(); + + return true; + } + catch (\Exception $e) + { + $this->setError($e->getMessage()); + + return false; + } + } + /** + * Batch move categories to a new category. + * + * @param integer $value The new category ID. + * @param array $pks An array of row IDs. + * @param array $contexts An array of item contexts. + * + * @return boolean True on success. + * + * @since 3.8.6 + */ + public function storeHistory($pk) + { + // Set some needed variables. + $this->table = $this->getTable(); + $this->tableClassName = get_class($this->table); + + // Check that the row actually exists + if (!$this->table->load($pk)) + { + if ($error = $this->table->getError()) + { + // Fatal error + $this->setError($error); + + return false; + } + else + { + // Not fatal error + $this->setError(Text::sprintf('JLIB_APPLICATION_ERROR_BATCH_MOVE_ROW_NOT_FOUND', $pk)); + } + + // Store the row. + if (!$this->table->store()) + { + $this->setError($this->table->getError()); + + return false; + } + } + + // Clean the cache + $this->cleanCache(); + + return true; + } + /** * Method to test whether a record can be deleted. * @@ -630,7 +724,8 @@ protected function loadFormData() $data->set('language', $app->input->getString('language', (!empty($filters['language']) ? $filters['language'] : null))); } - $data->set('access', + $data->set( + 'access', $app->input->getInt('access', (!empty($filters['access']) ? $filters['access'] : $app->get('access'))) ); } @@ -682,6 +777,7 @@ public function validate($form, $data, $group = null) * * @since 1.6 */ + public function save($data) { $input = Factory::getApplication()->input; @@ -694,6 +790,22 @@ public function save($data) $data['metadata']['author'] = $filter->clean($data['metadata']['author'], 'TRIM'); } + if (!isset($data['draft']) || $data['state'] == -3) + { + if (!isset($data['draft']) || $data['state'] != 1) + { + $data['draft'] = 1; + } + } + + if ($data['state'] != -3) + { + if ($data['state'] == 1) + { + $data['draft'] = 0; + } + } + if (isset($data['created_by_alias'])) { $data['created_by_alias'] = $filter->clean($data['created_by_alias'], 'TRIM'); @@ -799,7 +911,7 @@ public function save($data) } // Automatic handling of alias for empty fields - if (in_array($input->get('task'), array('apply', 'save', 'save2new')) && (!isset($data['id']) || (int) $data['id'] == 0)) + if (in_array($input->get('task'), array('apply', 'save', 'save2new', 'saveAsDraft')) && (!isset($data['id']) || (int) $data['id'] == 0)) { if ($data['alias'] == null) { @@ -1031,6 +1143,46 @@ public function featured($pks, $value = 0, $featuredUp = null, $featuredDown = n return true; } + public function draft($pks) + { + // Sanitize the ids. + $pks = (array) $pks; + $pks = ArrayHelper::toInteger($pks); + $context = $this->option . '.' . $this->name; + + if (empty($pks)) + { + $this->setError(Text::_('COM_CONTENT_NO_ITEM_SELECTED')); + + return false; + } + + try + { + $value = -3; + + // Adjust the mapping table. + // Clear the existing features settings. + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__content')) + ->set($db->quoteName('state') . ' = :state') + ->whereIn($db->quoteName('id'), $pks) + ->bind(':state', $value, ParameterType::INTEGER); + + $db->setQuery($query); + $db->execute(); + } + catch (\Exception $e) + { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } + /** * A protected method to get a set of ordering conditions. * diff --git a/administrator/components/com_content/src/Model/ArticlesModel.php b/administrator/components/com_content/src/Model/ArticlesModel.php index e9b8e02a02a43..1fc0b771c62ae 100644 --- a/administrator/components/com_content/src/Model/ArticlesModel.php +++ b/administrator/components/com_content/src/Model/ArticlesModel.php @@ -1,4 +1,5 @@ quoteName('a.metadesc'), $db->quoteName('a.metadata'), $db->quoteName('a.version'), + $db->quoteName('a.draft'), + $db->quoteName('a.shared') ] ) ) @@ -351,7 +356,7 @@ protected function getListQuery() // Filter by featured. $featured = (string) $this->getState('filter.featured'); - if (in_array($featured, ['0','1'])) + if (in_array($featured, ['0', '1'])) { $featured = (int) $featured; $query->where($db->quoteName('a.featured') . ' = :featured') @@ -618,7 +623,7 @@ public function getTransitions() $query = $db->getQuery(true); - $query ->select( + $query->select( [ $db->quoteName('t.id', 'value'), $db->quoteName('t.title', 'text'), @@ -704,4 +709,5 @@ public function getItems() return $items; } + } diff --git a/administrator/components/com_content/src/Model/DraftModel.php b/administrator/components/com_content/src/Model/DraftModel.php new file mode 100644 index 0000000000000..bdb07c7693a36 --- /dev/null +++ b/administrator/components/com_content/src/Model/DraftModel.php @@ -0,0 +1,348 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Content\Administrator\Model; + +\defined('_JEXEC') or die; + +use \Datetime; +use Joomla\CMS\Event\AbstractEvent; +use Joomla\CMS\Factory; +use Joomla\CMS\Form\Form; +use Joomla\CMS\Form\FormFactoryInterface; +use Joomla\CMS\Helper\TagsHelper; +use Joomla\CMS\Language\Associations; +use Joomla\CMS\Language\LanguageHelper; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\Factory\MVCFactoryInterface; +use Joomla\CMS\MVC\Model\AdminModel; +use Joomla\CMS\MVC\Model\WorkflowBehaviorTrait; +use Joomla\CMS\MVC\Model\WorkflowModelInterface; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\String\PunycodeHelper; +use Joomla\CMS\Table\Table; +use Joomla\CMS\Table\TableInterface; +use Joomla\CMS\Tag\TaggableTableInterface; +use Joomla\CMS\UCM\UCMType; +use Joomla\CMS\Versioning\VersionableModelTrait; +use Joomla\CMS\Workflow\Workflow; +use Joomla\Component\Categories\Administrator\Helper\CategoriesHelper; +use Joomla\Component\Fields\Administrator\Helper\FieldsHelper; +use Joomla\Database\ParameterType; +use Joomla\Registry\Registry; +use Joomla\Utilities\ArrayHelper; + +/** + * Item Model for a Draft. + * + * @since 1.6 + */ + +class DraftModel extends AdminModel implements WorkflowModelInterface +{ + use WorkflowBehaviorTrait, VersionableModelTrait; + + /** + * The prefix to use with controller messages. + * + * @var string + * @since 1.6 + */ + protected $text_prefix = 'COM_CONTENT'; + + /** + * The type alias for this content type (for example, 'com_content.draft'). + * + * @var string + * @since 3.2 + */ + public $typeAlias = 'com_content.draft'; + + /** + * The context used for the associations table + * + * @var string + * @since 3.4.4 + */ + protected $associationsContext = 'com_content.item'; + + /** + * The event to trigger before changing featured status one or more items. + * + * @var string + * @since 4.0.0 + */ + protected $event_before_change_featured = null; + + /** + * The event to trigger after changing featured status one or more items. + * + * @var string + * @since 4.0.0 + */ + protected $event_after_change_featured = null; + + /** + * Constructor. + * + * @param array $config An array of configuration options (name, state, dbo, table_path, ignore_request). + * @param MVCFactoryInterface $factory The factory. + * @param FormFactoryInterface $formFactory The form factory. + * + * @since 1.6 + * @throws \Exception + */ + public function __construct($config = array(), MVCFactoryInterface $factory = null, FormFactoryInterface $formFactory = null) + { + $config['events_map'] = $config['events_map'] ?? []; + + $config['events_map'] = array_merge( + ['featured' => 'content'], + $config['events_map'] + ); + + parent::__construct($config, $factory, $formFactory); + } + + /** + * Method to get the record form. + * + * @param array $data Data for the form. + * @param boolean $loadData True if the form is to load its own data (default case), false if not. + * + * @return Form|boolean A Form object on success, false on failure + * + * @since 1.6 + */ + public function getForm($data = array(), $loadData = true) + { + $app = Factory::getApplication(); + + // Get the form. + $form = $this->loadForm('com_content.draft', 'draft', array('control' => 'jform', 'load_data' => $loadData)); + + if (empty($form)) + { + return false; + } + + // Object uses for checking edit state permission of draft + $record = new \stdClass; + + // Get ID of the draft from input, for frontend, we use a_id while backend uses id + $draftIdFromInput = (int) $app->input->getInt('a_id') ?: $app->input->getInt('id', 0); + + // On edit draft, we get ID of draft from draft.id state, but on save, we use data from input + $id = (int) $this->getState('draft.id', $draftIdFromInput); + + $record->id = $id; + + // For new drafts we load the potential state + associations + if ($id == 0 && $formField = $form->getField('catid')) + { + $assignedCatids = $data['catid'] ?? $form->getValue('catid'); + + $assignedCatids = is_array($assignedCatids) + ? (int) reset($assignedCatids) + : (int) $assignedCatids; + + // Try to get the category from the category field + if (empty($assignedCatids)) + { + $assignedCatids = $formField->getAttribute('default', null); + + if (!$assignedCatids) + { + // Choose the first category available + $catOptions = $formField->options; + + if ($catOptions && !empty($catOptions[0]->value)) + { + $assignedCatids = (int) $catOptions[0]->value; + } + } + } + + // Activate the reload of the form when category is changed + $form->setFieldAttribute('catid', 'refresh-enabled', true); + $form->setFieldAttribute('catid', 'refresh-cat-id', $assignedCatids); + $form->setFieldAttribute('catid', 'refresh-section', 'draft'); + + // Store ID of the category uses for edit state permission check + $record->catid = $assignedCatids; + } + else + { + // Get the category which the draft is being added to + if (!empty($data['catid'])) + { + $catId = (int) $data['catid']; + } + else + { + $catIds = $form->getValue('catid'); + + $catId = is_array($catIds) + ? (int) reset($catIds) + : (int) $catIds; + + if (!$catId) + { + $catId = (int) $form->getFieldAttribute('catid', 'default', 0); + } + } + + $record->catid = $catId; + } + + // Modify the form based on Edit State access controls. + if (!$this->canEditState($record)) + { + // Disable fields for display. + $form->setFieldAttribute('featured', 'disabled', 'true'); + $form->setFieldAttribute('featured_up', 'disabled', 'true'); + $form->setFieldAttribute('featured_down', 'disabled', 'true'); + $form->setFieldAttribute('ordering', 'disabled', 'true'); + $form->setFieldAttribute('publish_up', 'disabled', 'true'); + $form->setFieldAttribute('publish_down', 'disabled', 'true'); + $form->setFieldAttribute('state', 'disabled', 'true'); + + // Disable fields while saving. + // The controller has already verified this is an draft you can edit. + $form->setFieldAttribute('featured', 'filter', 'unset'); + $form->setFieldAttribute('featured_up', 'filter', 'unset'); + $form->setFieldAttribute('featured_down', 'filter', 'unset'); + $form->setFieldAttribute('ordering', 'filter', 'unset'); + $form->setFieldAttribute('publish_up', 'filter', 'unset'); + $form->setFieldAttribute('publish_down', 'filter', 'unset'); + $form->setFieldAttribute('state', 'filter', 'unset'); + } + + // Don't allow to change the created_by user if not allowed to access com_users. + if (!Factory::getUser()->authorise('core.manage', 'com_users')) + { + $form->setFieldAttribute('created_by', 'filter', 'unset'); + } + + return $form; + } + + /** + * Method to unshare drafts. + * + * @param array $pks The ids of the items to toggle. + * @param integer $value The value to toggle to. + * @param string|Date $featuredUp The date which item featured up. + * @param string|Date $featuredDown The date which item featured down. + * + * @return boolean True on success. + */ + public function unshare($pks) + { + // Sanitize the ids. + $pks = (array) $pks; + + if (empty($pks)) + { + $this->setError(Text::_('COM_CONTENT_NO_ITEM_SELECTED')); + + return false; + } + + try + { + $value = 0; + + // Adjust the mapping table. + // Clear the existing features settings. + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__draft')) + ->set($db->quoteName('state') . ' = :state') + ->set($db->quoteName('shared_date') . ' = NULL') + ->whereIn($db->quoteName('hashval'), $pks, ParameterType::STRING) + ->bind(':state', $value, ParameterType::INTEGER); + + $db->setQuery($query); + $db->execute(); + } + catch (\Exception $e) + { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } + + public function share($pks) + { + // Sanitize the ids. + if (empty($pks)) + { + $this->setError(Text::_('COM_CONTENT_NO_ITEM_SELECTED')); + + return false; + } + + try + { + $value = 1; + + // Adjust the mapping table. + // Clear the existing features settings. + $now = new DateTime; + $date_sql = $now->format('Y-m-d H:i:s'); + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->update($db->quoteName('#__draft')) + ->set($db->quoteName('state') . ' = :state') + ->set($db->quoteName('shared_date') . ' = :date') + ->whereIn($db->quoteName('hashval'), $pks, ParameterType::STRING) + ->bind(':state', $value, ParameterType::INTEGER) + ->bind(':date', $date_sql, ParameterType::STRING); + + $db->setQuery($query); + $db->execute(); + } + catch (\Exception $e) + { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } + + public function delete($pks) + { + // Sanitize the ids. + try + { + $db = $this->getDbo(); + $query = $db->getQuery(true) + ->delete($db->quoteName('#__draft')) + ->where($db->quoteName('hashval') . ' = :hashval') + ->bind(':hashval', $pks, ParameterType::STRING); + $db->setQuery($query); + $db->execute(); + } + catch (\Exception $e) + { + $this->setError($e->getMessage()); + + return false; + } + + return true; + } +} diff --git a/administrator/components/com_content/src/Model/DraftsModel.php b/administrator/components/com_content/src/Model/DraftsModel.php new file mode 100644 index 0000000000000..de8a519876d3b --- /dev/null +++ b/administrator/components/com_content/src/Model/DraftsModel.php @@ -0,0 +1,388 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Content\Administrator\Model; + +\defined('_JEXEC') or die; + +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Associations; +use Joomla\CMS\MVC\Model\ListModel; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Table\Table; +use Joomla\Component\Content\Administrator\Extension\ContentComponent; +use Joomla\Database\ParameterType; +use Joomla\Registry\Registry; +use Joomla\Utilities\ArrayHelper; + +/** + * Methods supporting a list of article records. + * + * @since 1.6 + */ +class DraftsModel extends ListModel +{ + /** + * Constructor. + * + * @param array $config An optional associative array of configuration settings. + * + * @since 1.6 + * @see \Joomla\CMS\MVC\Controller\BaseController + */ + public function __construct($config = array()) + { + if (empty($config['filter_fields'])) + { + $config['filter_fields'] = array( + 'id', 'a.id', + 'title', 'a.title', + 'alias', 'a.alias', + 'checked_out', 'a.checked_out', + 'checked_out_time', 'a.checked_out_time', + 'catid', 'a.catid', 'category_title', + 'state', 'a.state', + 'access', 'a.access', 'access_level', + 'created', 'a.created', + 'modified', 'a.modified', + 'created_by', 'a.created_by', + 'created_by_alias', 'a.created_by_alias', + 'ordering', 'a.ordering', + 'featured', 'a.featured', + 'featured_up', 'fp.featured_up', + 'featured_down', 'fp.featured_down', + 'language', 'a.language', + 'hits', 'a.hits', + 'publish_up', 'a.publish_up', + 'publish_down', 'a.publish_down', + 'published', 'a.published', + 'author_id', + 'category_id', + 'level', + 'tag', + 'rating_count', 'rating', + 'stage', 'wa.stage_id', + 'ws.title' + ); + + if (Associations::isEnabled()) + { + $config['filter_fields'][] = 'association'; + } + } + + parent::__construct($config); + } + + /** + * Get the filter form + * + * @param array $data data + * @param boolean $loadData load current data + * + * @return Form|null The \JForm object or null if the form can't be found + * + * @since 3.2 + */ + public function getFilterForm($data = array(), $loadData = true) + { + $form = parent::getFilterForm($data, $loadData); + + $params = ComponentHelper::getParams('com_content'); + + if (!$params->get('workflow_enabled')) + { + $form->removeField('stage', 'filter'); + } + else + { + $ordering = $form->getField('fullordering', 'list'); + + $ordering->addOption('JSTAGE_ASC', ['value' => 'ws.title ASC']); + $ordering->addOption('JSTAGE_DESC', ['value' => 'ws.title DESC']); + } + + return $form; + } + + /** + * Method to auto-populate the model state. + * + * Note. Calling getState in this method will result in recursion. + * + * @param string $ordering An optional ordering field. + * @param string $direction An optional direction (asc|desc). + * + * @return void + * + * @since 1.6 + */ + protected function populateState($ordering = 'a.id', $direction = 'desc') + { + $app = Factory::getApplication(); + + $forcedLanguage = $app->input->get('forcedLanguage', '', 'cmd'); + + // Adjust the context to support modal layouts. + if ($layout = $app->input->get('layout')) + { + $this->context .= '.' . $layout; + } + + // Adjust the context to support forced languages. + if ($forcedLanguage) + { + $this->context .= '.' . $forcedLanguage; + } + + $search = $this->getUserStateFromRequest($this->context . '.filter.search', 'filter_search'); + $this->setState('filter.search', $search); + + $featured = $this->getUserStateFromRequest($this->context . '.filter.featured', 'filter_featured', ''); + $this->setState('filter.featured', $featured); + + $published = $this->getUserStateFromRequest($this->context . '.filter.published', 'filter_published', ''); + $this->setState('filter.published', $published); + + $level = $this->getUserStateFromRequest($this->context . '.filter.level', 'filter_level'); + $this->setState('filter.level', $level); + + $language = $this->getUserStateFromRequest($this->context . '.filter.language', 'filter_language', ''); + $this->setState('filter.language', $language); + + $formSubmited = $app->input->post->get('form_submited'); + + // Gets the value of a user state variable and sets it in the session + $this->getUserStateFromRequest($this->context . '.filter.access', 'filter_access'); + $this->getUserStateFromRequest($this->context . '.filter.author_id', 'filter_author_id'); + $this->getUserStateFromRequest($this->context . '.filter.category_id', 'filter_category_id'); + $this->getUserStateFromRequest($this->context . '.filter.tag', 'filter_tag', ''); + + if ($formSubmited) + { + $access = $app->input->post->get('access'); + $this->setState('filter.access', $access); + + $authorId = $app->input->post->get('author_id'); + $this->setState('filter.author_id', $authorId); + + $categoryId = $app->input->post->get('category_id'); + $this->setState('filter.category_id', $categoryId); + + $tag = $app->input->post->get('tag'); + $this->setState('filter.tag', $tag); + } + + // List state information. + parent::populateState($ordering, $direction); + + // Force a language + if (!empty($forcedLanguage)) + { + $this->setState('filter.language', $forcedLanguage); + $this->setState('filter.forcedLanguage', $forcedLanguage); + } + } + + /** + * Method to get a store id based on model configuration state. + * + * This is necessary because the model is used by the component and + * different modules that might need different sets of data or different + * ordering requirements. + * + * @param string $id A prefix for the store id. + * + * @return string A store id. + * + * @since 1.6 + */ + protected function getStoreId($id = '') + { + // Compile the store id. + $id .= ':' . $this->getState('filter.search'); + $id .= ':' . serialize($this->getState('filter.access')); + $id .= ':' . $this->getState('filter.published'); + $id .= ':' . serialize($this->getState('filter.category_id')); + $id .= ':' . serialize($this->getState('filter.author_id')); + $id .= ':' . $this->getState('filter.language'); + $id .= ':' . serialize($this->getState('filter.tag')); + + return parent::getStoreId($id); + } + + /** + * Build an SQL query to load the list data. + * + * @return \Joomla\Database\DatabaseQuery + * + * @since 1.6 + */ + protected function getListQuery() + { + // Create a new query object. + $db = $this->getDbo(); + $query = $db->getQuery(true); + + // Select the required fields from the table. + $query->select( + [ + $db->quoteName('a.article_id', 'id'), + $db->quoteName('a.state'), + $db->quoteName('a.hashval'), + $db->quoteName('a.shared_date'), + $db->quoteName('b.title'), + $db->quoteName('b.alias'), + $db->quoteName('h.version_id'), + $db->quoteName('c.title', 'category_title') + ] + ) + ->from($db->quoteName('#__draft', 'a')) + ->join('INNER', $db->quoteName('#__content', 'b'), $db->quoteName('a.article_id') . ' = ' . $db->quoteName('b.id')) + ->join('INNER', $db->quoteName('#__history', 'h'), $db->quoteName('a.version_id') . ' = ' . $db->quoteName('h.version_id')) + ->join('LEFT', $db->quoteName('#__categories', 'c'), $db->quoteName('c.id') . ' = ' . $db->quoteName('b.catid')); + + return $query; + } + + /** + * Method to get all transitions at once for all articles + * + * @return array|boolean + * + * @since 4.0.0 + */ + public function getTransitions() + { + // Get a storage key. + $store = $this->getStoreId('getTransitions'); + + // Try to load the data from internal storage. + if (isset($this->cache[$store])) + { + return $this->cache[$store]; + } + + $db = $this->getDbo(); + $user = Factory::getUser(); + + $items = $this->getItems(); + + if ($items === false) + { + return false; + } + + $stage_ids = ArrayHelper::getColumn($items, 'stage_id'); + $stage_ids = ArrayHelper::toInteger($stage_ids); + $stage_ids = array_values(array_unique(array_filter($stage_ids))); + + $workflow_ids = ArrayHelper::getColumn($items, 'workflow_id'); + $workflow_ids = ArrayHelper::toInteger($workflow_ids); + $workflow_ids = array_values(array_unique(array_filter($workflow_ids))); + + $this->cache[$store] = array(); + + try + { + if (count($stage_ids) || count($workflow_ids)) + { + Factory::getLanguage()->load('com_workflow', JPATH_ADMINISTRATOR); + + $query = $db->getQuery(true); + + $query->select( + [ + $db->quoteName('t.id', 'value'), + $db->quoteName('t.title', 'text'), + $db->quoteName('t.from_stage_id'), + $db->quoteName('t.to_stage_id'), + $db->quoteName('s.id', 'stage_id'), + $db->quoteName('s.title', 'stage_title'), + $db->quoteName('t.workflow_id'), + ] + ) + ->from($db->quoteName('#__workflow_transitions', 't')) + ->innerJoin( + $db->quoteName('#__workflow_stages', 's'), + $db->quoteName('t.to_stage_id') . ' = ' . $db->quoteName('s.id') + ) + ->where( + [ + $db->quoteName('t.published') . ' = 1', + $db->quoteName('s.published') . ' = 1', + ] + ) + ->order($db->quoteName('t.ordering')); + + $where = []; + + if (count($stage_ids)) + { + $where[] = $db->quoteName('t.from_stage_id') . ' IN (' . implode(',', $query->bindArray($stage_ids)) . ')'; + } + + if (count($workflow_ids)) + { + $where[] = '(' . $db->quoteName('t.from_stage_id') . ' = -1 AND ' . $db->quoteName('t.workflow_id') . ' IN (' . implode(',', $query->bindArray($workflow_ids)) . '))'; + } + + $query->where('((' . implode(') OR (', $where) . '))'); + + $transitions = $db->setQuery($query)->loadAssocList(); + + foreach ($transitions as $key => $transition) + { + if (!$user->authorise('core.execute.transition', 'com_content.transition.' . (int) $transition['value'])) + { + unset($transitions[$key]); + } + } + + $this->cache[$store] = $transitions; + } + } + catch (\RuntimeException $e) + { + $this->setError($e->getMessage()); + + return false; + } + + return $this->cache[$store]; + } + + /** + * Method to get a list of articles. + * Overridden to add item type alias. + * + * @return mixed An array of data items on success, false on failure. + * + * @since 4.0.0 + */ + public function getItems() + { + $items = parent::getItems(); + + foreach ($items as $item) + { + $item->typeAlias = 'com_content.article'; + + if (isset($item->metadata)) + { + $registry = new Registry($item->metadata); + $item->metadata = $registry->toArray(); + } + } + + return $items; + } +} diff --git a/administrator/components/com_content/src/Table/DraftTable.php b/administrator/components/com_content/src/Table/DraftTable.php new file mode 100644 index 0000000000000..4cf84b4cc9122 --- /dev/null +++ b/administrator/components/com_content/src/Table/DraftTable.php @@ -0,0 +1,22 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Content\Administrator\Table; + +\defined('JPATH_PLATFORM') or die; + +/** + * Draft table + * + * @since 1.5 + */ +class DraftTable extends \JTableContent +{ +} diff --git a/administrator/components/com_content/src/View/Article/HtmlView.php b/administrator/components/com_content/src/View/Article/HtmlView.php index 93d056e9ecc70..35012f409ae58 100644 --- a/administrator/components/com_content/src/View/Article/HtmlView.php +++ b/administrator/components/com_content/src/View/Article/HtmlView.php @@ -1,4 +1,5 @@ save2new('article.save2new'); + + if (!empty($this->item->id)) + { + $childBar->saveAsDraft('article.saveAsDraft'); + } } ); @@ -200,6 +206,7 @@ function (Toolbar $childBar) use ($checkedOut, $itemEditable, $canDo, $user) if ($canDo->get('core.create')) { $childBar->save2copy('article.save2copy'); + $childBar->saveAsDraft('article.saveAsDraft'); } } ); @@ -232,6 +239,13 @@ function (Toolbar $childBar) use ($checkedOut, $itemEditable, $canDo, $user) } } + if (!empty($this->item->id)) + { + $toolbar->standardButton('article.shareAsDraft', "JTOOLBAR_SHARE_AS_DRAFT") + ->icon('icon-project-diagram') + ->task('article.saveAsDraft'); + } + $toolbar->divider(); $toolbar->help('JHELP_CONTENT_ARTICLE_MANAGER_EDIT'); } diff --git a/administrator/components/com_content/src/View/Articles/HtmlView.php b/administrator/components/com_content/src/View/Articles/HtmlView.php index a829d46dbba15..fecb0f49fa342 100644 --- a/administrator/components/com_content/src/View/Articles/HtmlView.php +++ b/administrator/components/com_content/src/View/Articles/HtmlView.php @@ -242,6 +242,12 @@ protected function addToolbar() } } + // TODO: HERE + $toolbar->saveAsDraft('com_content') + ->icon('icon-project-diagram') + ->task('articles.saveAsDraft') + ->listCheck(true); + if (!$this->isEmptyState && $this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED && $canDo->get('core.delete')) { $toolbar->delete('articles.delete') diff --git a/administrator/components/com_content/src/View/Draft/HtmlView.php b/administrator/components/com_content/src/View/Draft/HtmlView.php new file mode 100644 index 0000000000000..fa702f5257d15 --- /dev/null +++ b/administrator/components/com_content/src/View/Draft/HtmlView.php @@ -0,0 +1,117 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Content\Administrator\View\Article; + +\defined('_JEXEC') or die; + +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\Helper\ContentHelper; +use Joomla\CMS\Language\Associations; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\View\GenericDataException; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Toolbar\Toolbar; +use Joomla\CMS\Toolbar\ToolbarHelper; +use Joomla\Component\Content\Site\Helper\RouteHelper; + +/** + * View to edit an draft. + * + * @since 1.6 + */ +class HtmlView extends BaseHtmlView +{ + /** + * The \JForm object + * + * @var \Joomla\CMS\Form\Form + */ + protected $form; + + /** + * The active item + * + * @var object + */ + protected $item; + + /** + * The model state + * + * @var object + */ + protected $state; + + /** + * The actions the user is authorised to perform + * + * @var \JObject + */ + protected $canDo; + + /** + * Pagebreak TOC alias + * + * @var string + */ + protected $eName; + + /** + * Execute and display a template script. + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + * + * @throws \Exception + * @since 1.6 + */ + public function display($tpl = null) + { + if ($this->getLayout() == 'pagebreak') + { + parent::display($tpl); + + return; + } + + $this->form = $this->get('Form'); + $this->item = $this->get('Item'); + $this->state = $this->get('State'); + $this->canDo = ContentHelper::getActions('com_content', 'draft', $this->item->id); + + // Check for errors. + if (count($errors = $this->get('Errors'))) + { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // If we are forcing a language in modal (used for associations). + if ($this->getLayout() === 'modal' && $forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'cmd')) + { + // Set the language field to the forcedLanguage and disable changing it. + $this->form->setValue('language', null, $forcedLanguage); + $this->form->setFieldAttribute('language', 'readonly', 'true'); + + // Only allow to select categories with All language or with the forced language. + $this->form->setFieldAttribute('catid', 'language', '*,' . $forcedLanguage); + + // Only allow to select tags with All language or with the forced language. + $this->form->setFieldAttribute('tags', 'language', '*,' . $forcedLanguage); + } + + $this->addToolbar(); + + parent::display($tpl); + } +} diff --git a/administrator/components/com_content/src/View/Drafts/HtmlView.php b/administrator/components/com_content/src/View/Drafts/HtmlView.php new file mode 100644 index 0000000000000..ad56fb231ddb4 --- /dev/null +++ b/administrator/components/com_content/src/View/Drafts/HtmlView.php @@ -0,0 +1,201 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +namespace Joomla\Component\Content\Administrator\View\Drafts; + +\defined('_JEXEC') or die; + +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Multilanguage; +use Joomla\CMS\Language\Text; +use Joomla\CMS\MVC\View\GenericDataException; +use Joomla\CMS\MVC\View\HtmlView as BaseHtmlView; +use Joomla\CMS\Plugin\PluginHelper; +use Joomla\CMS\Toolbar\Toolbar; +use Joomla\CMS\Toolbar\ToolbarHelper; +use Joomla\Component\Content\Administrator\Extension\ContentComponent; +use Joomla\Component\Content\Administrator\Helper\ContentHelper; + +/** + * View class for a list of drafts. + * + * @since 1.6 + */ +class HtmlView extends BaseHtmlView +{ + /** + * An array of items + * + * @var array + */ + protected $items; + + /** + * The pagination object + * + * @var \JPagination + */ + protected $pagination; + + /** + * The model state + * + * @var \JObject + */ + protected $state; + + /** + * Form object for search filters + * + * @var \JForm + */ + public $filterForm; + + /** + * The active search filters + * + * @var array + */ + public $activeFilters; + + /** + * All transition, which can be executed of one if the items + * + * @var array + */ + protected $transitions = []; + + /** + * Is this view an Empty State + * + * @var boolean + * @since 4.0.0 + */ + private $isEmptyState = false; + + /** + * Display the view + * + * @param string $tpl The name of the template file to parse; automatically searches through the template paths. + * + * @return void + */ + public function display($tpl = null) + { + $this->items = $this->get('Items'); + $this->pagination = $this->get('Pagination'); + $this->state = $this->get('State'); + $this->filterForm = $this->get('FilterForm'); + $this->activeFilters = $this->get('ActiveFilters'); + + if (!\count($this->items) && $this->isEmptyState = $this->get('IsEmptyState')) + { + $this->setLayout('emptystate'); + } + + if (ComponentHelper::getParams('com_content')->get('workflow_enabled')) + { + PluginHelper::importPlugin('workflow'); + + $this->transitions = $this->get('Transitions'); + } + + // Check for errors. + if (\count($errors = $this->get('Errors')) || $this->transitions === false) + { + throw new GenericDataException(implode("\n", $errors), 500); + } + + // We don't need toolbar in the modal window. + if ($this->getLayout() !== 'modal') + { + $this->addToolbar(); + + // We do not need to filter by language when multilingual is disabled + if (!Multilanguage::isEnabled()) + { + unset($this->activeFilters['language']); + $this->filterForm->removeField('language', 'filter'); + } + } + else + { + // In article associations modal we need to remove language filter if forcing a language. + // We also need to change the category filter to show show categories with All or the forced language. + if ($forcedLanguage = Factory::getApplication()->input->get('forcedLanguage', '', 'CMD')) + { + // If the language is forced we can't allow to select the language, so transform the language selector filter into a hidden field. + $languageXml = new \SimpleXMLElement(''); + $this->filterForm->setField($languageXml, 'filter', true); + + // Also, unset the active language filter so the search tools is not open by default with this filter. + unset($this->activeFilters['language']); + + // One last changes needed is to change the category filter to just show categories with All language or with the forced language. + $this->filterForm->setFieldAttribute('category_id', 'language', '*,' . $forcedLanguage, 'filter'); + } + } + + parent::display($tpl); + } + + /** + * Add the page title and toolbar. + * + * @return void + * + * @since 1.6 + */ + protected function addToolbar() + { + $canDo = ContentHelper::getActions('com_content', 'category', $this->state->get('filter.category_id')); + $user = Factory::getApplication()->getIdentity(); + + // Get the toolbar object instance + $toolbar = Toolbar::getInstance('toolbar'); + + ToolbarHelper::title(Text::_('COM_CONTENT_DRAFTS_TITLE'), 'copy article'); + + // TODO: HERE + $toolbar->unshare('com_content') + ->text('JTOOLBAR_UNSHARE') + ->icon('icon-eye-slash') + ->task('drafts.unshareDrafts') + ->listCheck(true); + + $toolbar->save('com_content') + ->text('JTOOLBAR_SHARE') + ->icon('icon-eye') + ->task('drafts.shareDrafts') + ->listCheck(true); + + $toolbar->cancel('com_content') + ->text('JTOOLBAR_DELETE') + ->icon('icon-trash') + ->task('drafts.deleteDrafts') + ->listCheck(true); + + if (!$this->isEmptyState && $this->state->get('filter.published') == ContentComponent::CONDITION_TRASHED && $canDo->get('core.delete')) + { + $toolbar->delete('articles.delete') + ->text('JTOOLBAR_EMPTY_TRASH') + ->message('JGLOBAL_CONFIRM_DELETE') + ->listCheck(true); + } + + if ($user->authorise('core.admin', 'com_content') || $user->authorise('core.options', 'com_content')) + { + $toolbar->preferences('com_content'); + } + + $toolbar->help('JHELP_CONTENT_ARTICLE_MANAGER'); + } +} diff --git a/administrator/components/com_content/tmpl/articles/default.php b/administrator/components/com_content/tmpl/articles/default.php index d3fa1fb904e6d..6726ee08b55c6 100644 --- a/administrator/components/com_content/tmpl/articles/default.php +++ b/administrator/components/com_content/tmpl/articles/default.php @@ -1,4 +1,5 @@ document->getWebAssetManager(); + /** @var \Joomla\CMS\WebAsset\WebAssetManager $wa */ + $wa = $this->document->getWebAssetManager(); -$wa->getRegistry()->addExtensionRegistryFile('com_workflow'); -$wa->useScript('com_workflow.admin-items-workflow-buttons') - ->addInlineScript($js, [], ['type' => 'module']); + $wa->getRegistry()->addExtensionRegistryFile('com_workflow'); + $wa->useScript('com_workflow.admin-items-workflow-buttons') + ->addInlineScript($js, [], ['type' => 'module']); -$workflow_state = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.state', 'com_content.article'); -$workflow_featured = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article'); + $workflow_state = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.state', 'com_content.article'); + $workflow_featured = Factory::getApplication()->bootComponent('com_content')->isFunctionalityUsed('core.featured', 'com_content.article'); endif; @@ -121,9 +122,9 @@ - - - + + + @@ -134,6 +135,7 @@ + @@ -171,228 +173,231 @@ - class="js-draggable" data-url="" data-direction="" data-nested="true"> - items as $i => $item) : - $item->max_ordering = 0; - $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); - $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); - $canEditOwn = $user->authorise('core.edit.own', 'com_content.article.' . $item->id) && $item->created_by == $userId; - $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; - $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); - $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; - $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); - $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; + class="js-draggable" data-url="" data-direction="" data-nested="true" > + items as $i => $item) : + $item->max_ordering = 0; + $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', 'com_content.article.' . $item->id) && $item->created_by == $userId; + $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; + $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); + $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; + $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); + $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; - $transitions = ContentHelper::filterTransitions($this->transitions, (int) $item->stage_id, (int) $item->workflow_id); + $transitions = ContentHelper::filterTransitions($this->transitions, (int) $item->stage_id, (int) $item->workflow_id); - $transition_ids = ArrayHelper::getColumn($transitions, 'value'); - $transition_ids = ArrayHelper::toInteger($transition_ids); + $transition_ids = ArrayHelper::getColumn($transitions, 'value'); + $transition_ids = ArrayHelper::toInteger($transition_ids); ?> - - - id, false, 'cid', 'cb', $item->title); ?> - - - - - - - - - - - - - $transitions, - 'title' => Text::_($item->stage_title), - 'tip_content' => Text::sprintf('JWORKFLOW', Text::_($item->workflow_title)), - 'id' => 'workflow-' . $item->id, - 'task' => 'articles.runTransition' - ]; + + + id, false, 'cid', 'cb', $item->title); ?> + + + + + + + + + + + + + $transitions, + 'title' => Text::_($item->stage_title), + 'tip_content' => Text::sprintf('JWORKFLOW', Text::_($item->workflow_title)), + 'id' => 'workflow-' . $item->id, + 'task' => 'articles.runTransition' + ]; - echo (new TransitionButton($options)) - ->render(0, $i); - ?> - - - - 'articles.', - 'disabled' => $workflow_featured || !$canChange, - 'id' => 'featured-' . $item->id - ]; + echo (new TransitionButton($options)) + ->render(0, $i); + ?> + + + + 'articles.', + 'disabled' => $workflow_featured || !$canChange, + 'id' => 'featured-' . $item->id + ]; - echo (new FeaturedButton) - ->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down); - ?> - - - 'articles.', - 'disabled' => $workflow_state || !$canChange, - 'id' => 'state-' . $item->id - ]; + echo (new FeaturedButton) + ->render((int) $item->featured, $i, $options, $item->featured_up, $item->featured_down); + ?> + + + 'articles.', + 'disabled' => $workflow_state || !$canChange, + 'id' => 'state-' . $item->id + ]; - echo (new PublishedButton)->render((int) $item->state, $i, $options, $item->publish_up, $item->publish_down); - ?> - - -
- checked_out) : ?> - editor, $item->checked_out_time, 'articles.', $canCheckin); ?> - - - - escape($item->title); ?> - - escape($item->title); ?> - -
- note)) : ?> - escape($item->alias)); ?> + echo (new PublishedButton)->render((int) $item->state, $i, $options, $item->publish_up, $item->publish_down); + ?> + + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'articles.', $canCheckin); ?> + + + + escape($item->title); ?> - escape($item->alias), $this->escape($item->note)); ?> + escape($item->title); ?> -
-
- parent_category_id . '&extension=com_content'); - $CurrentCatUrl = Route::_('index.php?option=com_categories&task=category.edit&id=' . $item->catid . '&extension=com_content'); - $EditCatTxt = Text::_('COM_CONTENT_EDIT_CATEGORY'); - echo Text::_('JCATEGORY') . ': '; - if ($item->category_level != '1') : - if ($item->parent_category_level != '1') : - echo ' » '; - endif; - endif; - if (Factory::getLanguage()->isRtl()) - { - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - echo $this->escape($item->category_title); - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; +
+ note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
+
+ parent_category_id . '&extension=com_content'); + $CurrentCatUrl = Route::_('index.php?option=com_categories&task=category.edit&id=' . $item->catid . '&extension=com_content'); + $EditCatTxt = Text::_('COM_CONTENT_EDIT_CATEGORY'); + echo Text::_('JCATEGORY') . ': '; if ($item->category_level != '1') : - echo ' « '; - if ($canEditParCat || $canEditOwnParCat) : - echo ''; + if ($item->parent_category_level != '1') : + echo ' » '; + endif; + endif; + if (Factory::getLanguage()->isRtl()) + { + if ($canEditCat || $canEditOwnCat) : + echo ''; endif; - echo $this->escape($item->parent_category_title); - if ($canEditParCat || $canEditOwnParCat) : + echo $this->escape($item->category_title); + if ($canEditCat || $canEditOwnCat) : echo ''; endif; - endif; - } - else - { - if ($item->category_level != '1') : - if ($canEditParCat || $canEditOwnParCat) : - echo ''; + if ($item->category_level != '1') : + echo ' « '; + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo $this->escape($item->parent_category_title); + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + endif; + } + else + { + if ($item->category_level != '1') : + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo $this->escape($item->parent_category_title); + if ($canEditParCat || $canEditOwnParCat) : + echo ''; + endif; + echo ' » '; + endif; + if ($canEditCat || $canEditOwnCat) : + echo ''; endif; - echo $this->escape($item->parent_category_title); - if ($canEditParCat || $canEditOwnParCat) : + echo $this->escape($item->category_title); + if ($canEditCat || $canEditOwnCat) : echo ''; endif; - echo ' » '; - endif; - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - echo $this->escape($item->category_title); - if ($canEditCat || $canEditOwnCat) : - echo ''; - endif; - } - ?> + } + ?> +
-
- - - escape($item->access_level); ?> - - - created_by != 0) : ?> - - escape($item->author_name); ?> - - - - - created_by_alias) : ?> -
escape($item->created_by_alias)); ?>
- - - - - association) : ?> - id); ?> - - - - + + - + escape($item->access_level); ?> - - - {$orderingColumn}; - echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC4')) : '-'; - ?> - - hits) : ?> - - - hits; ?> - + + created_by != 0) : ?> + + escape($item->author_name); ?> + + + + + created_by_alias) : ?> +
escape($item->created_by_alias)); ?>
+ - - vote) : ?> - - - rating_count; ?> - + + + association) : ?> + id); ?> + + + + + + + + + + {$orderingColumn}; + echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_LC4')) : '-'; + ?> - - - rating; ?> - + hits) : ?> + + + hits; ?> + + + + vote) : ?> + + + rating_count; ?> + + + + + rating; ?> + + + + + id; ?> - - - id; ?> - - - - + + + - + pagination->getListFooter(); ?> - - authorise('core.create', 'com_content') + + authorise('core.create', 'com_content') && $user->authorise('core.edit', 'com_content') - && $user->authorise('core.edit.state', 'com_content')) : ?> + && $user->authorise('core.edit.state', 'com_content') + ) : ?> - + @@ -415,4 +420,4 @@
- + \ No newline at end of file diff --git a/administrator/components/com_content/tmpl/articles/default_batch_body.php b/administrator/components/com_content/tmpl/articles/default_batch_body.php index eaa3e08bbea6a..ea296d74016e3 100644 --- a/administrator/components/com_content/tmpl/articles/default_batch_body.php +++ b/administrator/components/com_content/tmpl/articles/default_batch_body.php @@ -1,4 +1,5 @@
= 0) : ?> -
-
- 'com_content']); ?> +
+
+ 'com_content']); ?> +
-
@@ -49,11 +50,11 @@
authorise('core.admin', 'com_content') && $params->get('workflow_enabled')) : ?> -
-
- 'com_content']); ?> +
+
+ 'com_content']); ?> +
-
-
+
\ No newline at end of file diff --git a/administrator/components/com_content/tmpl/draft/edit.php b/administrator/components/com_content/tmpl/draft/edit.php new file mode 100644 index 0000000000000..1d0101ea48a36 --- /dev/null +++ b/administrator/components/com_content/tmpl/draft/edit.php @@ -0,0 +1,15 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +/** @var \Joomla\Component\Content\Administrator\View\Article\HtmlView $this */ + +defined('_JEXEC') or die; + +echo "draft view"; diff --git a/administrator/components/com_content/tmpl/draft/edit.xml b/administrator/components/com_content/tmpl/draft/edit.xml new file mode 100644 index 0000000000000..f5efd601bb092 --- /dev/null +++ b/administrator/components/com_content/tmpl/draft/edit.xml @@ -0,0 +1,17 @@ + + + + + + + +
+ + + +
+
diff --git a/administrator/components/com_content/tmpl/draft/modal.php b/administrator/components/com_content/tmpl/draft/modal.php new file mode 100644 index 0000000000000..9147d5b5ed403 --- /dev/null +++ b/administrator/components/com_content/tmpl/draft/modal.php @@ -0,0 +1,15 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; +?> +
+ setLayout('edit'); ?> + loadTemplate(); ?> +
diff --git a/administrator/components/com_content/tmpl/draft/pagebreak.php b/administrator/components/com_content/tmpl/draft/pagebreak.php new file mode 100644 index 0000000000000..53a8211f88bf3 --- /dev/null +++ b/administrator/components/com_content/tmpl/draft/pagebreak.php @@ -0,0 +1,48 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Text; + +/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ +$wa = $this->document->getWebAssetManager(); +$wa->useScript('com_content.admin-article-pagebreak'); + +$this->eName = Factory::getApplication()->input->getCmd('e_name', ''); +$this->eName = preg_replace('#[^A-Z0-9\-\_\[\]]#i', '', $this->eName); +$this->document->setTitle(Text::_('COM_CONTENT_PAGEBREAK_DOC_TITLE')); + +?> +
+
+
+
+ +
+
+ +
+
+
+
+ +
+
+ +
+
+ + + +
+
diff --git a/administrator/components/com_content/tmpl/drafts/default.php b/administrator/components/com_content/tmpl/drafts/default.php new file mode 100644 index 0000000000000..e356be702bf18 --- /dev/null +++ b/administrator/components/com_content/tmpl/drafts/default.php @@ -0,0 +1,204 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ + +defined('_JEXEC') or die; + +use Joomla\CMS\Button\PublishedButton; +use Joomla\CMS\Factory; +use Joomla\CMS\HTML\HTMLHelper; +use Joomla\CMS\Language\Associations; +use Joomla\CMS\Language\Text; +use Joomla\CMS\Layout\LayoutHelper; +use Joomla\CMS\Router\Route; +use Joomla\CMS\Session\Session; + +HTMLHelper::_('behavior.multiselect'); + +$app = Factory::getApplication(); +$user = Factory::getUser(); +$userId = $user->get('id'); +$listOrder = $this->escape($this->state->get('list.ordering')); +$listDirn = $this->escape($this->state->get('list.direction')); +$saveOrder = $listOrder == 'a.ordering'; + +if (strpos($listOrder, 'publish_up') !== false) +{ + $orderingColumn = 'publish_up'; +} +elseif (strpos($listOrder, 'publish_down') !== false) +{ + $orderingColumn = 'publish_down'; +} +elseif (strpos($listOrder, 'modified') !== false) +{ + $orderingColumn = 'modified'; +} +else +{ + $orderingColumn = 'created'; +} + +if ($saveOrder && !empty($this->items)) +{ + $saveOrderingUrl = 'index.php?option=com_content&task=articles.saveOrderAjax&tmpl=component&' . Session::getFormToken() . '=1'; + HTMLHelper::_('draggablelist.draggable'); +} + +$assoc = Associations::isEnabled(); +?> + +
+
+
+
+ items)) : ?> +
+ + +
+ + + + + + + + + + + + + + + + + + + + + class="js-draggable" data-url="" data-direction="" data-nested="true" > + items as $i => $item) : + $item->max_ordering = 0; + $canEdit = $user->authorise('core.edit', 'com_content.article.' . $item->id); + $canCheckin = $user->authorise('core.manage', 'com_checkin') || $item->checked_out == $userId || is_null($item->checked_out); + $canEditOwn = $user->authorise('core.edit.own', 'com_content.article.' . $item->id) && $item->created_by == $userId; + $canChange = $user->authorise('core.edit.state', 'com_content.article.' . $item->id) && $canCheckin; + $canEditCat = $user->authorise('core.edit', 'com_content.category.' . $item->catid); + $canEditOwnCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->catid) && $item->category_uid == $userId; + $canEditParCat = $user->authorise('core.edit', 'com_content.category.' . $item->parent_category_id); + $canEditOwnParCat = $user->authorise('core.edit.own', 'com_content.category.' . $item->parent_category_id) && $item->parent_category_uid == $userId; + ?> + + + + + + + + + + + + + + + + + + +
+ , + , + +
+ + + + + + + + + + + +
+ hashval, false, 'cid', 'cb', $item->title); ?> + + state) + { + echo '#shared'; + } + ?> + +
+ checked_out) : ?> + editor, $item->checked_out_time, 'articles.', $canCheckin); ?> + + + + escape($item->title); ?> + + escape($item->title); ?> + +
+ note)) : ?> + escape($item->alias)); ?> + + escape($item->alias), $this->escape($item->note)); ?> + +
+
+
+ state) : ?> + hashval ?> + + hashval ?> + + + shared_date; + echo $date > 0 ? HTMLHelper::_('date', $date, Text::_('DATE_FORMAT_FILTER_DATETIME')) : '-'; + ?> + + id; ?> +
+ + + pagination->getListFooter(); ?> + + + authorise('core.create', 'com_content') + && $user->authorise('core.edit', 'com_content') + && $user->authorise('core.edit.state', 'com_content') + ) : ?> + Text::_('COM_CONTENT_BATCH_OPTIONS'), + 'footer' => $this->loadTemplate('batch_footer'), + ), + $this->loadTemplate('batch_body') + ); ?> + + + + + + +
+
+
+
\ No newline at end of file diff --git a/administrator/components/com_content/tmpl/drafts/default.xml b/administrator/components/com_content/tmpl/drafts/default.xml new file mode 100644 index 0000000000000..d49551000236a --- /dev/null +++ b/administrator/components/com_content/tmpl/drafts/default.xml @@ -0,0 +1,102 @@ + + + + + + + + + +
+ + + + + + + + + + + + + + + + + + + +
+
+ +
+ + + + + + + + +
+
+
diff --git a/administrator/components/com_content/tmpl/drafts/default_batch_body.php b/administrator/components/com_content/tmpl/drafts/default_batch_body.php new file mode 100644 index 0000000000000..eaa3e08bbea6a --- /dev/null +++ b/administrator/components/com_content/tmpl/drafts/default_batch_body.php @@ -0,0 +1,59 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ +defined('_JEXEC') or die; + +use Joomla\CMS\Component\ComponentHelper; +use Joomla\CMS\Factory; +use Joomla\CMS\Language\Multilanguage; +use Joomla\CMS\Layout\LayoutHelper; + +$params = ComponentHelper::getParams('com_content'); + +$published = (int) $this->state->get('filter.published'); + +$user = Factory::getUser(); +?> + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+
+ = 0) : ?> +
+
+ 'com_content']); ?> +
+
+ +
+
+ +
+
+ authorise('core.admin', 'com_content') && $params->get('workflow_enabled')) : ?> +
+
+ 'com_content']); ?> +
+
+ +
+
diff --git a/administrator/components/com_content/tmpl/drafts/default_batch_footer.php b/administrator/components/com_content/tmpl/drafts/default_batch_footer.php new file mode 100644 index 0000000000000..3fe481e8ec80c --- /dev/null +++ b/administrator/components/com_content/tmpl/drafts/default_batch_footer.php @@ -0,0 +1,23 @@ + + * @license GNU General Public License version 2 or later; see LICENSE.txt + */ +defined('_JEXEC') or die; + +use Joomla\CMS\Language\Text; + +/** @var Joomla\CMS\WebAsset\WebAssetManager $wa */ +$wa = $this->document->getWebAssetManager(); +$wa->useScript('com_content.admin-articles-batch'); + +?> + + diff --git a/administrator/components/com_contenthistory/tmpl/history/modal.php b/administrator/components/com_contenthistory/tmpl/history/modal.php index 10a98ea2c238d..0955caa111035 100644 --- a/administrator/components/com_contenthistory/tmpl/history/modal.php +++ b/administrator/components/com_contenthistory/tmpl/history/modal.php @@ -49,6 +49,10 @@ + + + + @@ -79,6 +83,9 @@ version_note); ?> + + Draft + keep_forever) : ?> ' + ) + ); + break; + + default: + // Open in parent window + echo '' . + htmlspecialchars($label, ENT_COMPAT, 'UTF-8') . ' '; + break; + } + ?> + + + + + \ No newline at end of file diff --git a/installation/cache/index.html b/installation/cache/index.html deleted file mode 100644 index 2efb97f319a35..0000000000000 --- a/installation/cache/index.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/installation/sessions/.gitignore b/installation/sessions/.gitignore new file mode 100644 index 0000000000000..b722e9e13efe9 --- /dev/null +++ b/installation/sessions/.gitignore @@ -0,0 +1 @@ +!.gitignore \ No newline at end of file diff --git a/installation/sql/mysql/extensions.sql b/installation/sql/mysql/extensions.sql index 52a6c316af3e5..172564c1457ca 100644 --- a/installation/sql/mysql/extensions.sql +++ b/installation/sql/mysql/extensions.sql @@ -91,6 +91,18 @@ CREATE TABLE IF NOT EXISTS `#__banner_tracks` ( KEY `idx_banner_id` (`banner_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; + +-- +-- Table structure for table `#__draft` +-- +CREATE TABLE IF NOT EXISTS `#__draft` ( + `article_id` int unsigned NOT NULL, + `version_id` int unsigned NOT NULL, + `state` tinyint NOT NULL DEFAULT '0', + `hashval` varchar(2083) NOT NULL DEFAULT '', + `shared_date` datetime DEFAULT NULL, + PRIMARY KEY(`article_id`, `version_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 DEFAULT COLLATE=utf8mb4_unicode_ci; -- -------------------------------------------------------- -- @@ -187,6 +199,8 @@ CREATE TABLE IF NOT EXISTS `#__content` ( `featured` tinyint unsigned NOT NULL DEFAULT 0 COMMENT 'Set if article is featured.', `language` char(7) NOT NULL COMMENT 'The language code for the article.', `note` varchar(255) NOT NULL DEFAULT '', + `shared` tinyint(3) UNSIGNED NOT NULL DEFAULT '0', + `draft` tinyint(3) UNSIGNED NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_access` (`access`), KEY `idx_checkout` (`checked_out`), diff --git a/installation/sql/postgresql/extensions.sql b/installation/sql/postgresql/extensions.sql index 628c12098f02f..3234c2cae15f5 100644 --- a/installation/sql/postgresql/extensions.sql +++ b/installation/sql/postgresql/extensions.sql @@ -84,6 +84,18 @@ CREATE INDEX "#__banner_tracks_idx_track_date" ON "#__banner_tracks" ("track_dat CREATE INDEX "#__banner_tracks_idx_track_type" ON "#__banner_tracks" ("track_type"); CREATE INDEX "#__banner_tracks_idx_banner_id" ON "#__banner_tracks" ("banner_id"); +-- +-- Table structure for table `#__draft` +-- +CREATE TABLE IF NOT EXISTS `#__draft` ( + "article_id" integer unsigned NOT NULL, + "version_id" integer unsigned NOT NULL, + "state" smallint NOT NULL DEFAULT 0, + "hashval" varchar(2083) NOT NULL DEFAULT '', + "shared_date" timestamp without time zone DEFAULT NULL, + PRIMARY KEY("article_id", "version_id") +); + -- -- Table structure for table `#__contact_details` -- @@ -178,6 +190,8 @@ CREATE TABLE IF NOT EXISTS "#__content" ( "featured" smallint DEFAULT 0 NOT NULL, "language" varchar(7) DEFAULT '' NOT NULL, "note" varchar(255) DEFAULT '' NOT NULL, + "shared" smallint UNSIGNED NOT NULL DEFAULT 0, + "draft" smallint UNSIGNED NOT NULL DEFAULT 0, PRIMARY KEY ("id") ); CREATE INDEX "#__content_idx_access" ON "#__content" ("access"); diff --git a/language/en-GB/joomla.ini b/language/en-GB/joomla.ini index 4ffcb0e3db2a9..e4993433ee1a4 100644 --- a/language/en-GB/joomla.ini +++ b/language/en-GB/joomla.ini @@ -78,6 +78,9 @@ JENABLED="Enabled" JEXPIRED="Expired" JFALSE="False" JFEATURED="Featured" +JTOOLBAR_UNSHARE="Unshare" +JTOOLBAR_SHARE="Share" +JTOOLBAR_DELETE="Delete" JFEATURED_ASC="Featured ascending" JFEATURED_DESC="Featured descending" JHIDE="Hide" diff --git a/language/en-GB/lib_joomla.ini b/language/en-GB/lib_joomla.ini index ef9676294b321..7a7186f0ce9a3 100644 --- a/language/en-GB/lib_joomla.ini +++ b/language/en-GB/lib_joomla.ini @@ -452,6 +452,7 @@ JLIB_HTML_PUBLISH_ITEM="Publish Item" JLIB_HTML_PUBLISHED_EXPIRED_ITEM="Published, but has Expired." JLIB_HTML_PUBLISHED_FINISHED="Finish: %s" JLIB_HTML_PUBLISHED_ITEM="Published and is Current." +JLIB_HTML_SHARED_ITEM="Shared and is Current." JLIB_HTML_PUBLISHED_PENDING_ITEM="Published, but is Pending." JLIB_HTML_PUBLISHED_START="Start: %s" JLIB_HTML_PUBLISHED_UNPUBLISH="Select to unpublish" @@ -462,6 +463,8 @@ JLIB_HTML_SETDEFAULT_ITEM="Set default" JLIB_HTML_START="Start" JLIB_HTML_UNKNOWN_STATE="Unknown State" JLIB_HTML_UNPUBLISH_ITEM="Unpublish Item" +JLIB_HTML_UNSHARE_ITEM="Unshare Item" +JLIB_HTML_SHARE_ITEM="Share Item" JLIB_HTML_UNSETDEFAULT_ITEM="Unset default" JLIB_HTML_VIEW_ALL="View All" diff --git a/libraries/src/Button/PublishedButton.php b/libraries/src/Button/PublishedButton.php index 00f0e1b341d8f..8584b2dfbf7fc 100644 --- a/libraries/src/Button/PublishedButton.php +++ b/libraries/src/Button/PublishedButton.php @@ -1,4 +1,5 @@ addState(0, 'publish', 'unpublish', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JUNPUBLISHED')]); $this->addState(2, 'unpublish', 'archive', Text::_('JLIB_HTML_UNPUBLISH_ITEM'), ['tip_title' => Text::_('JARCHIVED')]); $this->addState(-2, 'publish', 'trash', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JTRASHED')]); + $this->addState(-3, 'publish', 'trash', Text::_('JLIB_HTML_PUBLISH_ITEM'), ['tip_title' => Text::_('JDRAFTED')]); } /** diff --git a/libraries/src/Form/Field/RedirectStatusField.php b/libraries/src/Form/Field/RedirectStatusField.php index 5f4576eb10757..be7da64fe5477 100644 --- a/libraries/src/Form/Field/RedirectStatusField.php +++ b/libraries/src/Form/Field/RedirectStatusField.php @@ -36,6 +36,7 @@ class RedirectStatusField extends PredefinedlistField '0' => 'JDISABLED', '1' => 'JENABLED', '2' => 'JARCHIVED', + '-3' => 'JDRAFTED', '*' => 'JALL', ); } diff --git a/libraries/src/Form/Field/StatusField.php b/libraries/src/Form/Field/StatusField.php index cb48372ac5f29..2e8ad89c9cda4 100644 --- a/libraries/src/Form/Field/StatusField.php +++ b/libraries/src/Form/Field/StatusField.php @@ -36,6 +36,7 @@ class StatusField extends PredefinedlistField 0 => 'JUNPUBLISHED', 1 => 'JPUBLISHED', 2 => 'JARCHIVED', + -3 => 'JDRAFTED', '*' => 'JALL', ); } diff --git a/libraries/src/HTML/Helpers/JGrid.php b/libraries/src/HTML/Helpers/JGrid.php index 890a25b916ad6..02a47f01661ea 100644 --- a/libraries/src/HTML/Helpers/JGrid.php +++ b/libraries/src/HTML/Helpers/JGrid.php @@ -188,6 +188,7 @@ public static function published($value, $i, $prefix = '', $enabled = true, $che 0 => array('publish', 'JUNPUBLISHED', 'JLIB_HTML_PUBLISH_ITEM', 'JUNPUBLISHED', true, 'unpublish', 'unpublish'), 2 => array('unpublish', 'JARCHIVED', 'JLIB_HTML_UNPUBLISH_ITEM', 'JARCHIVED', true, 'archive', 'archive'), -2 => array('publish', 'JTRASHED', 'JLIB_HTML_PUBLISH_ITEM', 'JTRASHED', true, 'trash', 'trash'), + -3 => array('publish', 'JDRAFTED', 'JLIB_HTML_PUBLISH_ITEM', 'JDRAFTED', true, 'draft', 'draft') ); // Special state for dates @@ -325,6 +326,11 @@ public static function publishedOptions($config = array()) $options[] = HTMLHelper::_('select.option', '-2', 'JTRASHED'); } + if (!array_key_exists('draft', $config) || $config['draft']) + { + $options[] = HTMLHelper::_('select.option', '-3', 'JDRAFTED'); + } + if (!array_key_exists('all', $config) || $config['all']) { $options[] = HTMLHelper::_('select.option', '*', 'JALL'); diff --git a/libraries/src/MVC/Controller/AdminController.php b/libraries/src/MVC/Controller/AdminController.php index c24d38985846e..fce45c65bb4e5 100644 --- a/libraries/src/MVC/Controller/AdminController.php +++ b/libraries/src/MVC/Controller/AdminController.php @@ -1,4 +1,5 @@ setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false + . $this->getRedirectToListAppend(), + false ) ); } @@ -258,7 +260,8 @@ public function publish() $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false + . $this->getRedirectToListAppend(), + false ) ); } @@ -371,8 +374,11 @@ public function checkin() $message = Text::sprintf('JLIB_APPLICATION_ERROR_CHECKIN_FAILED', $model->getError()); $this->setRedirect( Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false - ), $message, 'error' + 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), + false + ), + $message, + 'error' ); return false; @@ -383,8 +389,10 @@ public function checkin() $message = Text::plural($this->text_prefix . '_N_ITEMS_CHECKED_IN', \count($cid)); $this->setRedirect( Route::_( - 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), false - ), $message + 'index.php?option=' . $this->option . '&view=' . $this->view_list . $this->getRedirectToListAppend(), + false + ), + $message ); return true; diff --git a/libraries/src/MVC/Controller/BaseController.php b/libraries/src/MVC/Controller/BaseController.php index b52b94b7251b6..f7c66d53b1d34 100644 --- a/libraries/src/MVC/Controller/BaseController.php +++ b/libraries/src/MVC/Controller/BaseController.php @@ -1,4 +1,5 @@ $type, 'format' => $format)); @@ -483,7 +484,7 @@ public function __construct($config = array(), MVCFactoryInterface $factory = nu $this->default_view = $this->getName(); } - $this->factory = $factory ? : new LegacyFactory; + $this->factory = $factory ?: new LegacyFactory; } /** @@ -891,7 +892,7 @@ public function getView($name = '', $type = '', $prefix = '', $config = array()) { if ($view = $this->createView($name, $prefix, $type, $config)) { - self::$views[$name][$type][$prefix] = & $view; + self::$views[$name][$type][$prefix] = &$view; } else { diff --git a/libraries/src/MVC/Controller/FormController.php b/libraries/src/MVC/Controller/FormController.php index 023f14e1bf54e..3b15eb9d3fe68 100644 --- a/libraries/src/MVC/Controller/FormController.php +++ b/libraries/src/MVC/Controller/FormController.php @@ -1,4 +1,5 @@ registerTask('save2new', 'save'); $this->registerTask('save2copy', 'save'); $this->registerTask('editAssociations', 'save'); + $this->registerTask('saveToDraft', 'save'); } /** @@ -168,7 +174,8 @@ public function add() $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false + . $this->getRedirectToListAppend(), + false ) ); @@ -182,7 +189,8 @@ public function add() $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend(), false + . $this->getRedirectToItemAppend(), + false ) ); @@ -323,7 +331,8 @@ public function cancel($key = null) $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend($recordId, $key), false + . $this->getRedirectToItemAppend($recordId, $key), + false ) ); @@ -396,7 +405,8 @@ public function edit($key = null, $urlVar = null) $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false + . $this->getRedirectToListAppend(), + false ) ); @@ -412,7 +422,8 @@ public function edit($key = null, $urlVar = null) $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend($recordId, $urlVar), false + . $this->getRedirectToItemAppend($recordId, $urlVar), + false ) ); @@ -427,7 +438,8 @@ public function edit($key = null, $urlVar = null) $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend($recordId, $urlVar), false + . $this->getRedirectToItemAppend($recordId, $urlVar), + false ) ); @@ -593,7 +605,8 @@ public function save($key = null, $urlVar = null) $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend($recordId, $urlVar), false + . $this->getRedirectToItemAppend($recordId, $urlVar), + false ) ); @@ -614,7 +627,8 @@ public function save($key = null, $urlVar = null) $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false + . $this->getRedirectToListAppend(), + false ) ); @@ -689,7 +703,8 @@ public function save($key = null, $urlVar = null) $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend($recordId, $urlVar), false + . $this->getRedirectToItemAppend($recordId, $urlVar), + false ) ); @@ -713,7 +728,8 @@ public function save($key = null, $urlVar = null) $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend($recordId, $urlVar), false + . $this->getRedirectToItemAppend($recordId, $urlVar), + false ) ); @@ -732,7 +748,8 @@ public function save($key = null, $urlVar = null) $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend($recordId, $urlVar), false + . $this->getRedirectToItemAppend($recordId, $urlVar), + false ) ); @@ -758,7 +775,8 @@ public function save($key = null, $urlVar = null) $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend($recordId, $urlVar), false + . $this->getRedirectToItemAppend($recordId, $urlVar), + false ) ); break; @@ -772,7 +790,8 @@ public function save($key = null, $urlVar = null) $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_item - . $this->getRedirectToItemAppend(null, $urlVar), false + . $this->getRedirectToItemAppend(null, $urlVar), + false ) ); break; @@ -846,7 +865,8 @@ public function reload($key = null, $urlVar = null) $this->setRedirect( Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_list - . $this->getRedirectToListAppend(), false + . $this->getRedirectToListAppend(), + false ) ); $this->redirect(); @@ -855,7 +875,7 @@ public function reload($key = null, $urlVar = null) // The redirect url $redirectUrl = Route::_( 'index.php?option=' . $this->option . '&view=' . $this->view_item . - $this->getRedirectToItemAppend($recordId, $urlVar), + $this->getRedirectToItemAppend($recordId, $urlVar), false ); diff --git a/libraries/src/MVC/Model/BaseDatabaseModel.php b/libraries/src/MVC/Model/BaseDatabaseModel.php index fa05444bbd79c..71aac2653ab67 100644 --- a/libraries/src/MVC/Model/BaseDatabaseModel.php +++ b/libraries/src/MVC/Model/BaseDatabaseModel.php @@ -1,4 +1,5 @@ group === null && $query->merge === null && $query->querySet === null - && $query->having === null) + && $query->having === null + ) { $query = clone $query; $query->clear('select')->clear('order')->clear('limit')->clear('offset')->select('COUNT(*)'); diff --git a/libraries/src/MVC/Model/StateBehaviorTrait.php b/libraries/src/MVC/Model/StateBehaviorTrait.php index 86b419a1cc4ff..b0e12bfc9a42f 100644 --- a/libraries/src/MVC/Model/StateBehaviorTrait.php +++ b/libraries/src/MVC/Model/StateBehaviorTrait.php @@ -1,4 +1,5 @@ formValidation(true); } + // TODO: HERE + /** + * Writes a save-as-draft button for a given option. + * Save operation leads to a save and then close action. + * + * @param string $task The task name of this button. + * @param string $text The text of this button. + * + * @return StandardButton + * + * @since 4.0.0 + */ + public function saveAsDraft(string $task, string $text = 'JTOOLBAR_SAVE_AS_DRAFT'): StandardButton + { + return $this->standardButton('save-draft', $text) + ->task($task) + ->formValidation(true); + } + + + /** + * Writes a unshare button for a given option. + * Save operation leads to a unshare and then close. + * + * @param string $task The task name of this button. + * @param string $text The text of this button. + * + * @return StandardButton + * + * @since 4.0.0 + */ + public function unshare(string $task, string $text = 'JTOOLBAR_UNSHARE'): StandardButton + { + return $this->standardButton('icon-eye-slash', $text) + ->task($task) + ->formValidation(true); + } + + /** * Writes a checkin button for a given option. * @@ -477,7 +517,11 @@ public function preferences(string $component, string $text = 'JTOOLBAR_OPTIONS' * * @since 4.0.0 */ - public function versions(string $typeAlias, int $itemId, int $height = 800, int $width = 500, + public function versions( + string $typeAlias, + int $itemId, + int $height = 800, + int $width = 500, string $text = 'JTOOLBAR_VERSIONS' ): CustomButton { diff --git a/libraries/src/Workflow/Workflow.php b/libraries/src/Workflow/Workflow.php index c4583f84a9fb4..b2851912aa582 100644 --- a/libraries/src/Workflow/Workflow.php +++ b/libraries/src/Workflow/Workflow.php @@ -69,6 +69,7 @@ class Workflow self::CONDITION_UNPUBLISHED => 'JUNPUBLISHED', self::CONDITION_TRASHED => 'JTRASHED', self::CONDITION_ARCHIVED => 'JARCHIVED', + self::CONDITION_DRAFTED => 'JDRAFTED', ]; /** @@ -91,6 +92,8 @@ class Workflow */ const CONDITION_ARCHIVED = 2; + const CONDITION_DRAFTED = -3; + /** * Class constructor * diff --git a/plugins/content/pagenavigation/pagenavigation.php b/plugins/content/pagenavigation/pagenavigation.php index 7c3d8ad71dffb..ebde7936315d1 100644 --- a/plugins/content/pagenavigation/pagenavigation.php +++ b/plugins/content/pagenavigation/pagenavigation.php @@ -127,6 +127,12 @@ public function onContentBeforeDisplay($context, &$row, &$params, $page = 0) case 'rhits': $orderby = $db->quoteName('a.hits') . ' DESC'; break; + case 'draft': + $orderby = $db->quoteName('a.draft'); + break; + case 'rdraft': + $orderby = $db->quoteName('a.draft') . ' DESC'; + break; case 'author': $orderby = $db->quoteName(['a.created_by_alias', 'u.name']); break; diff --git a/plugins/sampledata/multilang/multilang.php b/plugins/sampledata/multilang/multilang.php index 9aa01dde53e6a..8f281a47bea16 100644 --- a/plugins/sampledata/multilang/multilang.php +++ b/plugins/sampledata/multilang/multilang.php @@ -1109,6 +1109,8 @@ private function addArticle($itemLanguage, $categoryId) 'language' => $itemLanguage->language, 'state' => 1, 'featured' => 1, + 'draft' => 1, + 'shared' => 0, 'attribs' => array(), 'rules' => array(), ); diff --git a/robots.txt.dist b/robots.txt similarity index 100% rename from robots.txt.dist rename to robots.txt