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

Всім гарного дня.

Пишу в даний момент для 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 достатньо в іншому сніппеті написати:

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()
{//очищає всі масиви від тимчасових дозволів у яких минув термін дії
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))//якщо enter був " 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))//якщо enter був " 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? ¶m=`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);//готове, ми створили документ
...


У цій статті я намагався показати, як ідея переходить в логіку, далі в код і в результаті — в працюючий механізм.

Сподіваюся, комусь це буде корисно.

Заздалегідь вибачаюся за помилки в тексті статті (хоч я і намагався все відловити). Досвіду написання великих текстів у мене немає. Буду радий будь-яким зауважень.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.