Система доступа к документам для MODX

в 12:26, , рубрики: modx, modx revolution, xpdo, велосипед, Программирование

Всем хорошего дня.

Пишу в данный момент для modx с использованием индивидуальных документов для каждого обычного пользователя.
Поскольку забивать этими документами админку не хотелось (если с сайтом все нормально будет, их там будет многовато и админка начнет тормозить), решил вынести их в отдельную таблицу БД, с выводом через один ресурс.

Итак, рассмотрим наши требования к документу (по крайне мере, у меня были такие):

  1. У документа должны быть заголовок и содержание;
  2. У документа должен быть тип (для более простого поиска документов одного типа);
  3. У владельца документа всегда есть доступ к его редактированию и просмотру;
  4. У владельца сайта и его юристов есть произвольный доступ к любому из документов;
  5. Владелец сайта и те, кому он это разрешил, должны иметь возможность выдавать нужным им пользователям права на просмотр и редактирование произвольного документа;
  6. Произвольный зарегистрированный пользователь может получить право на только просмотр либо также и редактирование произвольного документа на время либо сразу навсегда.

1. Определение структуры данных

Для начала прикинем структуру записи документа в базе данных:

Структура конкретного документа

//Сниппет TryLoadDocument
<?php
/**
Итак, все документы хранятся в базе вот в таком виде(переделано в формат JSON для лучшей читаемости):
{
"type":"agreement", - тип документа, текстовый, короткий, примеры: carta, license и т.д.
"title":"Договор", - заголовок документа
"text":"Пример. Текст договора. Подпишите здесь: ______ ", - текст документа, XSS контролирует CLeditor, не наша забота. Содержит весь текст какого-то документа со всеми тегами разметки.
"owner":29, - это владелец документа, то есть тот, с кем наш сайт его заключил. у него в любом случае есть право смотреть  и редактировать этот документ(т.к. он "его")
"edit":[29,555,34,52], - это те, кто может редактировать документ. 
!ВАЖНО! пользователи групп  Administrator,Jurists имеют доступ к ЛЮБОМУ документу!
"view":[5677,599677,5999898677,855677] - те, кто открыв страничку http://.../docs?doc_id=5 увидят этот документ(но редактировать не смогут)
"view-temp":[{"id":5,"until":1413640050},{"id":9,"until":1413640100},{"id":7,"until":1413640050}] - аналогично view, но "until"(формат timestamp) указывает, вплоть до какого момента времени нужно учитывать эту запись(после этого момента она удаляется)
"edit-temp":[{"id":5,"until":1413640050},{"id":9,"until":1413640100},{"id":7,"until":1413640050}] - аналогично edit, но "until"(формат timestamp) указывает, вплоть до какого момента времени нужно учитывать эту запись(после этого момента она удаляется)
}
вот таким запросом можно такую таблицу создать:
CREATE TABLE IF NOT EXISTS `documents` (
  `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id документа',
  `type` varchar(255) NOT NULL COMMENT 'тип документа',
  `title` varchar(255) NOT NULL COMMENT 'заголовок документа',
  `text` text NOT NULL COMMENT 'текст документа',
  `owner` bigint(20) NOT NULL COMMENT 'владелец дока',
  `edit` text NOT NULL COMMENT 'все юзеры, у кого есть права на просмотр и редактирование',
  `edit-temp` text NOT NULL COMMENT 'временные разрешения на редактирование',
  `view` text NOT NULL COMMENT 'все юзеры, у кого есть права на просмотр',
  `view-temp` text NOT NULL COMMENT 'временные разрешения на редактирование',
  UNIQUE KEY `id` (`id`)
) ENGINE=InnoDB  DEFAULT CHARSET=cp1251 COMMENT='Личные документы пользователей - здесь...';
*/

2. Сниппет TryLoadDocument

2.1 Обеспечение переносимости

После определения структуры стало понятно, что делать это набором функций будет спорным решением, поэтому был выбран вариант с ООП.
Ну и на этом же этапе вспоминаем, что объекты удобно повторно использовать в других модулях(сниппетах, в данном случае), поэтому используем сохранение этого сниппета в статичном файле docs.php(что также дает возможность в дальнейшем быстро подключить аякс, если будет такая необходимость), а также добавляем проверку, не подключен ли этот сниппет к другому только ради нашего класса вот таким образом:

Проверяем, не используется ли сниппет в качестве источника класса Document

<?php
class Document {...}
if(DOC_API_MODE!="API")//если нужен только класс Document, эта часть кода не выполнится)
{
   ...// выполнение кода сниппета
}

Теперь для подключения класса Document достаточно в другом сниппете напиcать:

define("DOC_API_MODE","API");
include_once 'docs.php';
2.2 Пишем общую логику работы класса

Теперь, когда все приготовления сделаны, начнем работу с самим классом Document.
Сначала определим данные нашего документа:

	private $data;//здесь все данные документа в виде ассоциативного массива(напр $data['title'] - заголовок документа)
	private $uid;//=user id — id того, кто пытается использовать этот документ(или создать)
	private $modx;//для работы с modx API 

В конструкторе мы будем только узнавать кто нас грузит, а не что за документ ему нужен, что позволит, например, используя один и тот же объект класса, создать множество документов пользователю сразу.

   public function  __construct($modx)
	{//т.к. доступа напрямую к $modx в функциях нет(по крайней мере у меня), передадим ее параметром
		$this->modx=$modx;
		$this->uid=$this->modx->user->get('id');//объект документа можно создать только от имени текущего юзера
	}

Дальше прикинем, что мы хотим чтобы объект этого класса нам выдавал? Другими словами, публичные методы.
Очевидно, это:

  • Заголовок и содержание документа;
  • Сможет ли текущий пользователь увидеть документ?
  • Сможет ли текущий пользователь отредактировать документ?
  • Есть ли у него право на выдачу прав другим?

Также добавим сюда то, что мы хотим, чтобы этот класс давал возможность делать:

  • Загрузить документ по его номеру(id) — будет дальше в тексте, т.к работа напрямую с БД;
  • Сохранить документ в текущем виде — будет дальше в тексте, т.к работа напрямую с БД;
  • Создать новый документ, задается заголовок, текст, и возможность его редактировать.

Вот реализация данных функций:

Публичные методы

Самое простое, доступ к полям снаружи класса:

	public function &__set (  $name ,  $value )
	{//снаружи(не используя функций) менять можно только title и text
	   $allowed=["title","text"];
	   if(in_array($name,$allowed))
	   {
	      if($this->editAllowed())//если текущему пользователю можно редактировать
	   		$this->data[$name]=$value;
	   }
	}
	public function &__get (  $name )
	{//снаружи класса напрямую получить значения полей могут все из списков edit & view, а также владелец...
		if($this->isOwner() || $this->viewAllowed() || $this->editAllowed())
		{
		switch ($name)
		{
			case "title":return $this->data['title'];
			case "text": return $this->data['text'];
			case "id": return $this->data['id'];
			case "uid": return $this->uid;
		}
		}
	    return 'forbidden';
	}

Теперь проверки на разрешения + создание нового документа из заданных данных:

Здесь работы напрямую с бд еще нет

	public function MakeNew($type,$title,$text)//для создания документа по шаблону, либо сгенерированного динамически
	{
		$this->data['text']=$text;//текст документа
		$this->data['title']=$title;//заголовок
		$this->data['view']=[];//кому можно просматривать документ
		$this->data['view-temp']=[];
		$this->data['edit']=[];//кому можно документ редактировать
		$this->data['edit-temp']=[];

		$this->load($this->saveAsNew($type));//сохраняем док и загружаем его в нормальном виде
	}
	public function viewAllowed()
	{//возвращает истину, если этому юзеру можно просматривать этот документ, иначе - ложь..
		$allowed= $this->isOwner()//если запрашивающий - создатель документа 
		|| in_array($this->uid,$this->data['view']);//или имеет право на его просмотр
		if($allowed)
			return true;
		else 
			for($i=0; $i<count($this->data['view-temp']);$i++)
				if($this->data['view-temp'][$i]->id==$this->uid)
					return $this->data['view-temp'][$i]->until > time();
		return false;
	}
	public function editAllowed()
	{//возвращает истину, если этому юзеру можно редактировать этот документ, иначе - ложь..
		$allowed = $this->isOwner() || //если запрашивающий - создатель документа
		in_array($this->uid,$this->data['edit']);//если запрашивающему можно редактировать документ
		if($allowed)
			return true;
		else 
			for($i=0; $i<count($this->data['edit-temp']);$i++)
				if($this->data['edit-temp'][$i]->id==$this->uid)
					return $this->data['edit-temp'][$i]->until > time();
//доступ к временным полям через '->' из-за принципа декодирования функцией json_decode
		return false;
	}
	public function manageAllowed()
	{//true если этому юзеру можно давать другим доступ к документу. false в обратном случае.
		return $this->modx->user->isMember('Jurists')||$this->modx->user->isMember('Administrator');
	}
	public function allow($new_user,$can_edit,$time=0)
	{
        //дать кому-то доступ к документу. $new_user - кому дать,  
	//$can_edit - может ли редактировать(только если запросивший сам может редактировать, иначе не сработает)
	//$time - на какое время дать права(по умолчанию 0 - навсегда. измеряется в секундах
		$user_id=(int)$new_user;
		if($user_id!=0 && $this->manageAllowed())
	 		if($can_edit)
			{
	  	  	if($this->editAllowed())
				{
						if($time==0)//выдать права навсегда
							$this->data['edit'][]=$user_id;
						else//выдать права на $time секунд
							$this->data['edit-temp'][]=["id"=>$user_id,"until"=>time()+$time];
				}
			} 
			else 
			{
						if($time==0)//выдать права навсегда
							$this->data['view'][]=$user_id;
						else//выдать права на $time секунд
							$this->data['view-temp'][]=["id"=>$user_id,"until"=>time()+$time];
			}
	}
	public function isOwner()
	{
	   $usual=$this->uid==$this->data['owner'];//права владельца есть у:
	   if($usual) return true;//самого владельца,
	   else//спец аккаунтов
	   return $this->manageAllowed();
	}

Вспомогательная функция, очищающая документ от устаревших временных разрешений:

	private function clearTemp()
	{//очищает все массивы от врeменных разрешений у которых прошел срок действия
	  if(count($this->data['view-temp'])+count($this->data['edit-temp']) > 0)//если хоть какие-то временные данные есть
	  {
		for($i=0; $i<count($this->data['view-temp']);$i++)
//удалить все временные разрешения, у которых дата истечения раньше текущего времени(time())
		{
			if($this->data['view-temp'][$i]->until < time())
				unset($this->data['view-temp'][$i]);
		}
		$this->data['view-temp']=array_values($this->data['view-temp']);//просто фикс проблемы с индексами( [1,3,5]=>[0,1,2)]

		for($i=0; $i<count($this->data['edit-temp']);$i++)
//удалить все временные разрешения, у которых дата истечения раньше текущего времени(time())
		{
			if($this->data['edit-temp'][$i]->until < time())
				unset($this->data['edit-temp'][$i]);
		}
		$this->data['edit-temp'] = array_values($this->data['edit-temp']);//просто фикс проблемы с индексами( [1,3,5]=>[0,1,2)]

		$this->save();//сохранить изменения

		}
	}

2.3 Пишем функции для работы с конкретной базой данных.

Итак, логическая часть нашего кода, не зависящая от конкретной бд, завершена. Теперь перейдем на уровень ниже, работе напрямую с бд в MODX с использованием xPDO.

Работа напрямую с базой данных

	public function load($id)
	{//загружает документ из бд(на это прав хватит у любого, вот только не любой сможет эти данные у класса получить)
		$sql="SELECT * FROM `documents` WHERE `id`=:id";
		$query = new xPDOCriteria($this->modx, $sql,array(':id'=>$id));
        if($query->prepare() && $query->stmt->execute())
		{//если данные удачно загружены
        	$this->data = $query->stmt->fetchAll(PDO::FETCH_ASSOC)[0];
			if(count($this->data)==0) return false;//если пришел пустой ответ, сообщаем о фейле зуагрузки
			$this->data['edit']=json_decode($this->data['edit']);///распаковываем список имеющих право на редактирование в массив
			$this->data['edit-temp']=json_decode($this->data['edit-temp']);///распаковываем список имеющих временное право на редактирование в массив
			$this->data['view']=json_decode($this->data['view']);///распаковываем список имеющих право на просмотр в массив
			$this->data['view-temp']=json_decode($this->data['view-temp']);///распаковываем список имеющих временное право на просмотр в массив
			$this->clearTemp();//очищаем массивы view-temp & edit-temp от закончившихся разрешений 
			return true;//раз дошли сюда, сообщаем, что все ок
		}
		else  return false;//если не выполнился запрос, сообщаем о фейле загрузки
	}

	public function save()
	{//сохраняет новое значение документа в бд
		$sql="UPDATE `documents` SET `title`=:title, `text`=:text, `view`=:view, `edit`=:edit, `view-temp`=:viewtemp, `edit-temp`=:edittemp WHERE `id`=:id";//шаблон запроса
		$this->data['view']=json_encode($this->data['view']);///запаковываем список имеющих право на просмотр в строку
		$this->data['view-temp']=json_encode($this->data['view-temp']);///запаковываем список имеющих право на временный просмотр в строку
		$this->data['edit']=json_encode($this->data['edit']);///запаковываем список имеющих право на редактирование в строку
		$this->data['edit-temp']=json_encode($this->data['edit-temp']);///запаковываем список имеющих право на временное редактирование в строку
		$query=new xPDOCriteria($this->modx, $sql,
		[":title"=>$this->data['title'],":text"=>$this->data['text'],":edit"=>$this->data['edit'],":view"=>$this->data['view'],":edittemp"=>$this->data['edit-temp'],":viewtemp"=>$this->data['view-temp'],":id"=>$this->data["id"]]);//подставляем данные

		$query->prepare() && $query->stmt->execute();//выполняем запрос
		//преобразуем строки обратно в массивы
		$this->data['view']=json_decode($this->data['view']);
		$this->data['view-temp']=json_decode($this->data['view-temp']);
		$this->data['edit']=json_decode($this->data['edit']);
		$this->data['edit-temp']=json_decode($this->data['edit-temp']);

	}
	private function saveAsNew($type)
	{//сохраняет уже заполненный документ как новую запись типа $type
		$sql="INSERT INTO `documents` (`title`,`text`,`view`,`edit`,`owner`,`type`) VALUES(:title,:text,:view,:edit,:uid,:type)";//шаблон запроса
		$this->data['view']=json_encode($this->data['view']);///запаковываем список имеющих право на просмотр в строку
		$this->data['edit']=json_encode($this->data['edit']);///запаковываем список имеющих право на редактирование в строку
		$this->data['view-temp']=json_encode($this->data['view-temp']);///запаковываем список имеющих право на просмотр в строку
		$this->data['edit-temp']=json_encode($this->data['edit-temp']);///запаковываем список имеющих право на редактирование в строку
		$query=new xPDOCriteria($this->modx, $sql,
		[":title"=>$this->data['title'],":text"=>$this->data['text'],":edit"=>$this->data['edit'],":view"=>$this->data['view'],":uid"=>$this->uid,":type"=>$type]);//подставляем данные
                //логгируем создание нового документа, полезно для отладки и чтобы посмотреть как шаблон данными заполняется
		//$this->modx->log(modX::LOG_LEVEL_ERROR,"Выполнение запроса: ".$query->toSQL());

		$query->prepare(); $query->stmt->execute();//выполняем запрос

		return $this->modx->lastInsertId();//вернем id созданного дока
	}

Вот и весь класс Document. Довольно простой, имхо.

2.4 Исполняемый код сниппета

Теперь разберемся, что же делает наш сниппет TryLoadDocument?

Итак…

Выполняющийся код сниппета TryLoadDoc — разбор

Проверяем, не зашел ли к нам аноним(его id — 0), в этому случае посылаем его логиниться.

if($modx->user->get('id')==0)//если на страницу зашел анон, 
{
   $modx->sendRedirect($modx->makeUrl(14));//отправляем его на страницу номер 14, именно там у нас вход в систему.
   exit;//на случай если редирект ВДРУГ не сработал, завершаем скрипт наверняка(вместо странички будет просто белый экран)
}
Пытаемся загрузить запрашиваемый документ(ресурс, где используется этот сниппет имеет вид /docs?doc_id=N).

$doc=new Document($modx);//создаем объект для работы с документом для вошедшего на страницу пользователя
if(!$doc->load($_GET['doc_id']))//пытаемся загрузить док из бд
{//если не удалось, выходим...
	return 'Документ не найден...';
}

Заполняем плейсхолдеры значениями, которые позволят дальше не выводить формы редактирования документа и редактирования прав для него тем, кто не может этим заниматься.

//!!! сообщаем чанку Rights, отображать ли поле редактирования доступом
$modx->setPlaceholder('CanManage',$doc->manageAllowed()?'true':'false');
//!!! сообщаем чанку DocText, отображать ли форму редактирования
$modx->setPlaceholder('CanEdit',$doc->editAllowed()?'true':'false');

Дальше простенькая функция, которая парсит то, что вы ввели как пользователя в поле формы редактирования прав.

function DataToUID($userstr,$modx)//для вытаскивания цифры с номером пользователя из поля "пользователю"
{
	$link_rgx="/(.*uid=)([d]{1,})(.*)/";//регулярка для ссылки вида http://*путь до личного кабинета*?uid=56
	$id_rgx="/([^w]{0,})([d]{1,})/";//регулярка id пользователя(до и после числа могут быть пробелы)
	if(preg_match($link_rgx,$userstr))
	{//для ссылки вида http://*путь до личного кабинета*?uid=56
		$r="$2";
		return preg_replace($link_rgx,$r,$userstr);
	} else
	if(preg_match($id_rgx,$userstr))//если ввод был "  234", например
	{
		$r="$2";
		return preg_replace($id_rgx,$r,$userstr);//вернем только число
	} else//это и не идентификатор юзера и не ссылка на его лк? тогда может это ник?
	{
		$usr=$modx->getObject('modUser',["username"=>$userstr]);//пытаемся найти юзера с таким ником
		return  $usr?$usr->get('id'):'-1';
	}

}

Обрабатываем добавление прав, если такое есть.

if(isset($_POST['add']))//если юзер нажал кнопочку "добавить права пользователю...
{
	$userID=(int)DataToUID($_POST['userid'],$modx);//вытаскиваем id пользователя из поля формы 'userid'
	if($userID!='-1')//если пользователь нашелся
	{

    	 if($_POST['allow']=="edit")//если надо добавить права на редактирование и просмотр(задается полем формы 'allow')
		 {
//пишем в лог
		 	$modx->log(modX::LOG_LEVEL_ERROR,"Попытка выдать права на редактирование документа #".$doc->id." пользователем #".$doc->uid." пользователю #".$userID); 
		 	$doc->allow($userID,true,(int)$_POST['length']);
			$doc->save();
		 } else
   	 	 if($_POST['allow']=="view")////если надо добавить права на просмотр(задается полем формы 'allow')
		 {
//пишем в лог
		 	$modx->log(modX::LOG_LEVEL_ERROR,"Попытка выдать права на просмотр документа #".$doc->id." пользователем #".$doc->uid." пользователю #".$userID);
		 	$doc->allow($userID,false,(int)$_POST['length']);
			$doc->save();
		 } 
	}else $modx->log(modX::LOG_LEVEL_ERROR,"DataToUID не справилась. (".$_POST['userid'].")");//если вместо имени пользователя передали фигню, пишем в лог
}

Обрабатываем редактирование документа, если такое есть

if(isset($_POST['edit']))//если юзер отредактировал документ и нажал "сохранить"
{
	$modx->log(modX::LOG_LEVEL_ERROR,"Попытка отредактировать текст документа #".$doc->id." пользователем #".$doc->uid);//пишем в лог
	if(!empty($_POST["text"]))//если новый вариант документа не пуст(нету смысла в пустых доках)
		{
			$doc->text=$_POST["text"];//задаем полю класса text новое значение
			$doc->save();
			$modx->log(modX::LOG_LEVEL_ERROR,"Отредактирован текст документа #".$doc->id." пользователем #".$doc->uid);//пишем в лог
		}
}

Возвращаем в плейсхолдере [[+doc]] контент ресурса, состоящий из документа и форм для его редактирования, к которым есть у юзера доступ.

/**********Вывод данных в форму*************/
if(!isset($_POST['ajax']))//если мы вызваны не через аякс, а для загрузки страницы.
{
	$output="";
	//грузим док, если нельзя, то объект сам разберется
//передаем чанку заголовка DocTitle параметром свойство title, то есть заголовок документа. проверять доступ к нему - не наша забота.
	$output.=$modx->getChunk('DocTitle',['title'=>$doc->title]);
//передаем чанку заголовка DocText параметром свойство title, то есть заголовок документа. проверять доступ к нему - не наша забота.
	$output.=$modx->getChunk('DocText',['text'=>$doc->text]);
	$modx->setPlaceholder('doc', $output);
	return '';
}

Для лучшего понимания кому-то может быть удобнее посмотреть код в один блок:

Выполняющийся код сниппета TryLoadDocument целиком

if($modx->user->get('id')==0)//если на страницу зашел анон, 
{
   $modx->sendRedirect($modx->makeUrl(14));//отправляем его на страницу номер 14, именно там у нас вход в систему.
   exit;//на случай если редирект ВДРУГ не сработал, завершаем скрипт наверняка(вместо странчики будет просто белый экран)
}
/*******/

$doc=new Document($modx);//создаем объект для работы с документом для вошедшего на страницу пользователя
if(!$doc->load($_GET['doc_id']))//пытаемся загрузить док из бд
{//если не удалось, выходим...
	return 'Документ не найден...';
}
//!!! сообщаем чанку Rights, отображать ли поле редактирования доступом
$modx->setPlaceholder('CanManage',$doc->manageAllowed()?'true':'false');
//!!! сообщаем чанку DocText, отображать ли форму редактирования
$modx->setPlaceholder('CanEdit',$doc->editAllowed()?'true':'false');

function DataToUID($userstr,$modx)//для вытаскивания цифры с номером пользователя из поля "пользователю"
{
	$link_rgx="/(.*uid=)([d]{1,})(.*)/";//регулярка для ссылки вида http://*путь до личного кабинета*?uid=56
	$id_rgx="/([^w]{0,})([d]{1,})/";//регулярка id пользователя(до и после числа могут быть пробелы)
	if(preg_match($link_rgx,$userstr))
	{//для ссылки вида http://*путь до личного кабинета*?uid=56
		$r="$2";
		return preg_replace($link_rgx,$r,$userstr);
	} else
	if(preg_match($id_rgx,$userstr))//если ввод был "  234", например
	{
		$r="$2";
		return preg_replace($id_rgx,$r,$userstr);//вернем только число
	} else//это и не идентификатор юзера и не ссылка на его лк? тогда может это ник?
	{
		$usr=$modx->getObject('modUser',["username"=>$userstr]);//пытаемся найти юзера с таким ником
		return  $usr?$usr->get('id'):'-1';
	}

}
/*********Эти два для работы в режиме форм***********/
if(isset($_POST['add']))//если юзер нажал кнопочку "добавить права пользователю...
{
	$userID=(int)DataToUID($_POST['userid'],$modx);//вытаскиваем id пользователя из поля формы 'userid'
	if($userID!='-1')//если пользователь нашелся
	{

    	 if($_POST['allow']=="edit")//если надо добавить права на редактирование и просмотр(задается полем формы 'allow')
		 {
//пишем в лог
		 	$modx->log(modX::LOG_LEVEL_ERROR,"Попытка выдать права на редактирование документа #".$doc->id." пользователем #".$doc->uid." пользователю #".$userID); 
		 	$doc->allow($userID,true,(int)$_POST['length']);
			$doc->save();
		 } else
   	 	 if($_POST['allow']=="view")////если надо добавить права на просмотр(задается полем формы 'allow')
		 {
//пишем в лог
		 	$modx->log(modX::LOG_LEVEL_ERROR,"Попытка выдать права на просмотр документа #".$doc->id." пользователем #".$doc->uid." пользователю #".$userID);
		 	$doc->allow($userID,false,(int)$_POST['length']);
			$doc->save();
		 } 
	}else $modx->log(modX::LOG_LEVEL_ERROR,"DataToUID не справилась. (".$_POST['userid'].")");//если вместо имени пользователя передали фигню, пишем в лог
}

if(isset($_POST['edit']))//если юзер отредактировал документ и нажал "сохранить"
{
	$modx->log(modX::LOG_LEVEL_ERROR,"Попытка отредактировать текст документа #".$doc->id." пользователем #".$doc->uid);//пишем в лог
	if(!empty($_POST["text"]))//если новый вариант документа не пуст(нету смысла в пустых доках)
		{
			$doc->text=$_POST["text"];//задаем полю класса text новое значение
			$doc->save();
			$modx->log(modX::LOG_LEVEL_ERROR,"Отредактирован текст документа #".$doc->id." пользователем #".$doc->uid);//пишем в лог
		}
}

/**********Вывод данных в форму*************/
if(!isset($_POST['ajax']))//если мы вызваны не через аякс, а для загрузки страницы.
{
	$output="";
	//грузим док, если нельзя, то объект сам разберется
//передаем чанку заголовка DocTitle параметром свойство title, то есть заголовок документа. проверять доступ к нему - не наша забота.
	$output.=$modx->getChunk('DocTitle',['title'=>$doc->title]);
//передаем чанку заголовка DocText параметром свойство title, то есть заголовок документа. проверять доступ к нему - не наша забота.
	$output.=$modx->getChunk('DocText',['text'=>$doc->text]);
	$modx->setPlaceholder('doc', $output);
	return '';
}

return 'Ошибка...';//для нормального куда мы здесь вообще не должны оказаться, так что вернем хоть что-то для объяснения неполадки

3. Форматирование полученных данных и вывод на страницу

3.1 Необходимые чанки

Для правильного вывода документа используются три простых чанка:

Rights — форма для редактирования прав

<form action="[[~[[*id]]]]?doc_id=[[!GET? &param=`doc_id`]]" method="post" >
<fieldset><span>Выдать права пользователю...</span><input type="text" name="userid" style="
    background: white;
    width: 340px;
    padding: 5px;
    font-size: 14px;
    margin: 0 15px;
"/> 
на 
<select name="allow">
  <option value="view" selected="selected">просмотр</option>
  <option value="edit">просмотр и редактирование</option>
</select>
<select name="length">
  <option value="60">на 1 минуту.</option>
  <option value="600">на 10 минут.</option>
  <option value="3600">на час.</option>
  <option value="86400">на день.</option>
  <option value="0">навсегда.</option>
</select>
<input type="hidden" name="add" value="1" />
</fieldset>

<input type="submit" value="Выдать права!"/>
</form>

DocTitle — шаблон отображаемого заголовка документа.

<h2>[[!+title]]</h2>

DoctText* — шаблон текста самого документа

[[!If? &subject=`[[!+CanEdit]]` &operator=`EQ` &operand=`true`  &then=`<form action="[[~[[*id]]]]?doc_id=[[!GET? &param=`doc_id`]]" method="post">
<input type="submit" value="Сохранить новый текст договора..."/>`]]
<textarea id="text" name="text" >
[[+text]]
</textarea>
[[!If? &subject=`[[!+CanEdit]]` &operator=`EQ` &operand=`true`  &then=`<input type="hidden" name="edit" value="1"/>
<input type="submit" value="Сохранить новый текст договора..."/>
</form>`]]

* — если почему-то форма не выводится никогда, стоит проверить, а установлен ли компонент If?

Итоговый код сниппета TryLoadDocument можно посмотреть тут: pastebin.com/R55bPUCH
Ну вот, почти все готово, осталось только приделать это в ваш контент ресурса с вашим шаблоном.

3.2 Ресурс для работы с документом

Код в самом поле контента для ресурса будет таким:

[[!TryLoadDocument]]
[[!If? &subject=`[[!+CanManage]]` &operator=`EQ` &operand=`true`  &then=`[[$Rights]]`]]
[[!+doc]]
[[!If? &subject=`[[!+CanManage]]` &operator=`EQ` &operand=`true`  &then=`[[$Rights]]`]]

Textarea с текстом нашего документа(из чанка DocText) должна быть завернута в какой-либо WYSIWYG-редактор, я, например, использовал CLeditor.

CLeditor ставится так

Качаете и кидаете в корень сайта файлы отсюда premiumsoftware.net/cleditor/downloads
Добавляете в шаблон такие заголовки в <head>*

  <link rel="stylesheet" type="text/css" href="jquery.cleditor.css" />
  <script type="text/javascript" src="jquery-2.1.1.min.js"></script>  <!--этого по ссылке нет, здесь просто пишите путь до вашего jquery-->
  <script type="text/javascript" src="jquery.cleditor.min.js"></script>     
  <script type="text/javascript">$(document).ready(function () { $("#text").cleditor({ height:"1300px"});  }); </script> 
<!--height:"1300px" — для того, чтобы документ не сжимался в слишком мелкий прямоугольник -->

* — для этого я определил TV ExtraHeaders, добавил его в <head> в шаблоне, и там, гле нужны были доп заголовки ресурсу, переопределял их там.

4. Пример использования класса Document в другом сниппете

Допустим, вы хотите в другим сниппете создавать свои документы текущему пользователю:

<?php
...
define("DOC_API_MODE","API");
include_once 'docs.php';
...
$doc= new Document($modx);
$title="Договор";
$text=$modx->getChunk('agreement_template');//пусть у нас есть некий чанк с шаблоном документа
//в этом же вызове ему можно было передать какие-то параметры
$doc->MakeNew('agreement',$title,$text);//готово, мы создали документ
...

В этой статье я старался показать, как идея переходит в логику, далее в код и в итоге — в работающий механизм.

Надеюсь, кому-то это будет полезно.

Заранее извиняюсь за ошибки в тексте статьи (хоть я и старался все отловить). Опыта написания больших текстов у меня нет. Буду рад любым замечаниям.

Автор: MaximChistov

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js