You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
487 lines
15 KiB
487 lines
15 KiB
<?php
|
|
/*********************************************************************
|
|
class.faq.php
|
|
|
|
Backend support for article creates, edits, deletes, and attachments.
|
|
|
|
Copyright (c) 2006-2013 osTicket
|
|
http://www.osticket.com
|
|
|
|
Released under the GNU General Public License WITHOUT ANY WARRANTY.
|
|
See LICENSE.TXT for details.
|
|
|
|
vim: expandtab sw=4 ts=4 sts=4:
|
|
**********************************************************************/
|
|
require_once('class.file.php');
|
|
require_once('class.category.php');
|
|
require_once('class.thread.php');
|
|
|
|
class FAQ extends VerySimpleModel {
|
|
|
|
static $meta = array(
|
|
'table' => FAQ_TABLE,
|
|
'pk' => array('faq_id'),
|
|
'ordering' => array('question'),
|
|
'defer' => array('answer'),
|
|
'select_related'=> array('category'),
|
|
'joins' => array(
|
|
'category' => array(
|
|
'constraint' => array(
|
|
'category_id' => 'Category.category_id'
|
|
),
|
|
),
|
|
'attachments' => array(
|
|
'constraint' => array(
|
|
"'F'" => 'Attachment.type',
|
|
'faq_id' => 'Attachment.object_id',
|
|
),
|
|
'list' => true,
|
|
'null' => true,
|
|
'broker' => 'GenericAttachments',
|
|
),
|
|
'topics' => array(
|
|
'reverse' => 'FaqTopic.faq',
|
|
),
|
|
),
|
|
);
|
|
|
|
const PERM_MANAGE = 'faq.manage';
|
|
static protected $perms = array(
|
|
self::PERM_MANAGE => array(
|
|
'title' =>
|
|
/* @trans */ 'FAQ',
|
|
'desc' =>
|
|
/* @trans */ 'Ability to add/update/disable/delete knowledgebase categories and FAQs',
|
|
'primary' => true,
|
|
));
|
|
|
|
var $_local;
|
|
var $_attachments;
|
|
|
|
const VISIBILITY_PRIVATE = 0;
|
|
const VISIBILITY_PUBLIC = 1;
|
|
const VISIBILITY_FEATURED = 2;
|
|
|
|
/* ------------------> Getter methods <--------------------- */
|
|
function getId() { return $this->faq_id; }
|
|
function getHashtable() {
|
|
$base = $this->ht;
|
|
unset($base['category']);
|
|
unset($base['attachments']);
|
|
return $base;
|
|
}
|
|
function getKeywords() { return $this->keywords; }
|
|
function getQuestion() { return $this->question; }
|
|
function getAnswer() { return $this->answer; }
|
|
function getAnswerWithImages() {
|
|
return Format::viewableImages($this->answer, ['type' => 'F']);
|
|
}
|
|
function getTeaser() {
|
|
return Format::truncate(Format::striptags($this->answer), 150);
|
|
}
|
|
function getSearchableAnswer() {
|
|
return ThreadEntryBody::fromFormattedText($this->answer, 'html')
|
|
->getSearchable();
|
|
}
|
|
function getNotes() { return $this->notes; }
|
|
function getNumAttachments() { return $this->attachments->count(); }
|
|
|
|
function isPublished() {
|
|
return $this->ispublished != self::VISIBILITY_PRIVATE
|
|
&& $this->category->isPublic();
|
|
}
|
|
function getVisibilityDescription() {
|
|
switch ($this->ispublished) {
|
|
case self::VISIBILITY_PRIVATE:
|
|
return __('Internal');
|
|
case self::VISIBILITY_PUBLIC:
|
|
return __('Public');
|
|
case self::VISIBILITY_FEATURED:
|
|
return __('Featured');
|
|
}
|
|
}
|
|
|
|
function getCreateDate() { return $this->created; }
|
|
function getUpdateDate() { return $this->updated; }
|
|
|
|
function getCategoryId() { return $this->category_id; }
|
|
function getCategory() { return $this->category; }
|
|
|
|
function getHelpTopicsIds() {
|
|
$ids = array();
|
|
foreach ($this->getHelpTopics() as $T)
|
|
$ids[] = $T->topic->getId();
|
|
return $ids;
|
|
}
|
|
|
|
function getHelpTopicNames() {
|
|
$names = array();
|
|
foreach ($this->getHelpTopics() as $T)
|
|
$names[] = $T->topic->getFullName();
|
|
return $names;
|
|
}
|
|
|
|
function getHelpTopics() {
|
|
return $this->topics;
|
|
}
|
|
|
|
/* ------------------> Setter methods <--------------------- */
|
|
function setPublished($val) { $this->ispublished = !!$val; }
|
|
function setQuestion($question) { $this->question = Format::striptags(trim($question)); }
|
|
function setAnswer($text) { $this->answer = $text; }
|
|
function setKeywords($words) { $this->keywords = $words; }
|
|
function setNotes($text) { $this->notes = $text; }
|
|
|
|
function publish() {
|
|
$this->setPublished(1);
|
|
return $this->save();
|
|
}
|
|
|
|
function unpublish() {
|
|
$this->setPublished(0);
|
|
return $this->save();
|
|
}
|
|
|
|
function printPdf() {
|
|
global $thisstaff;
|
|
require_once(INCLUDE_DIR.'class.pdf.php');
|
|
|
|
$paper = 'Letter';
|
|
if ($thisstaff)
|
|
$paper = $thisstaff->getDefaultPaperSize();
|
|
|
|
ob_start();
|
|
$faq = $this;
|
|
include STAFFINC_DIR . 'templates/faq-print.tmpl.php';
|
|
$html = ob_get_clean();
|
|
|
|
$pdf = new mPDFWithLocalImages(['mode' => 'utf-8', 'format' =>
|
|
$paper, 'tempDir'=>sys_get_temp_dir()]);
|
|
// Setup HTML writing and load default thread stylesheet
|
|
$pdf->WriteHtml(
|
|
'<style>
|
|
.bleed { margin: 0; padding: 0; }
|
|
.faded { color: #666; }
|
|
.faq-title { font-size: 170%; font-weight: bold; }
|
|
.thread-body { font-family: serif; }'
|
|
.file_get_contents(ROOT_DIR.'css/thread.css')
|
|
.'</style>'
|
|
.'<div>'.$html.'</div>', 0, true, true);
|
|
|
|
$pdf->Output(Format::slugify($faq->getQuestion()) . '.pdf', 'I');
|
|
}
|
|
|
|
// Internationalization of the knowledge base
|
|
|
|
function getTranslateTag($subtag) {
|
|
return _H(sprintf('faq.%s.%s', $subtag, $this->getId()));
|
|
}
|
|
function getLocal($subtag) {
|
|
$tag = $this->getTranslateTag($subtag);
|
|
$T = CustomDataTranslation::translate($tag);
|
|
return $T != $tag ? $T : $this->ht[$subtag];
|
|
}
|
|
function getAllTranslations() {
|
|
if (!isset($this->_local)) {
|
|
$tag = $this->getTranslateTag('q:a');
|
|
$this->_local = CustomDataTranslation::allTranslations($tag, 'article');
|
|
}
|
|
return $this->_local;
|
|
}
|
|
function getLocalQuestion($lang=false) {
|
|
return $this->_getLocal('question', $lang);
|
|
}
|
|
function getLocalAnswer($lang=false) {
|
|
return $this->_getLocal('answer', $lang);
|
|
}
|
|
function getLocalAnswerWithImages($lang=false) {
|
|
return Format::viewableImages($this->getLocalAnswer($lang),
|
|
['type' => 'F']);
|
|
}
|
|
function _getLocal($what, $lang=false) {
|
|
if (!$lang) {
|
|
$lang = $this->getDisplayLang();
|
|
}
|
|
$translations = $this->getAllTranslations();
|
|
foreach ($translations as $t) {
|
|
if (0 === strcasecmp($lang, $t->lang)) {
|
|
$data = $t->getComplex();
|
|
if (isset($data[$what]))
|
|
return $data[$what];
|
|
}
|
|
}
|
|
return $this->ht[$what];
|
|
}
|
|
function getDisplayLang() {
|
|
if (isset($_REQUEST['kblang']))
|
|
$lang = $_REQUEST['kblang'];
|
|
else
|
|
$lang = Internationalization::getCurrentLanguage();
|
|
return $lang;
|
|
}
|
|
|
|
function getLocalAttachments($lang=false) {
|
|
return $this->attachments->getSeparates()->filter(Q::any(array(
|
|
'lang__isnull' => true,
|
|
'lang' => $lang ?: $this->getDisplayLang(),
|
|
)));
|
|
}
|
|
|
|
function updateTopics($ids){
|
|
|
|
if($ids) {
|
|
$topics = $this->getHelpTopicsIds();
|
|
foreach($ids as $id) {
|
|
if($topics && in_array($id,$topics)) continue;
|
|
$sql='INSERT IGNORE INTO '.FAQ_TOPIC_TABLE
|
|
.' SET faq_id='.db_input($this->getId())
|
|
.', topic_id='.db_input($id);
|
|
db_query($sql);
|
|
}
|
|
}
|
|
|
|
if ($ids)
|
|
$this->topics->filter(Q::not(array('topic_id__in' => $ids)))->delete();
|
|
else
|
|
$this->topics->delete();
|
|
}
|
|
|
|
function saveTranslations($vars) {
|
|
global $thisstaff;
|
|
|
|
foreach ($this->getAllTranslations() as $t) {
|
|
$trans = @$vars['trans'][$t->lang];
|
|
if (!$trans || !array_filter($trans))
|
|
// Not updating translations
|
|
continue;
|
|
|
|
// Content is not new and shouldn't be added below
|
|
unset($vars['trans'][$t->lang]);
|
|
$content = array('question' => $trans['question'],
|
|
'answer' => Format::sanitize($trans['answer']));
|
|
|
|
// Don't update content which wasn't updated
|
|
if ($content == $t->getComplex())
|
|
continue;
|
|
|
|
$t->text = $content;
|
|
$t->agent_id = $thisstaff->getId();
|
|
$t->updated = SqlFunction::NOW();
|
|
if (!$t->save())
|
|
return false;
|
|
}
|
|
// New translations (?)
|
|
$tag = $this->getTranslateTag('q:a');
|
|
foreach ($vars['trans'] as $lang=>$parts) {
|
|
$content = array('question' => @$parts['question'],
|
|
'answer' => Format::sanitize(@$parts['answer']));
|
|
if (!array_filter($content))
|
|
continue;
|
|
$t = CustomDataTranslation::create(array(
|
|
'type' => 'article',
|
|
'object_hash' => $tag,
|
|
'lang' => $lang,
|
|
'text' => $content,
|
|
'revision' => 1,
|
|
'agent_id' => $thisstaff->getId(),
|
|
'updated' => SqlFunction::NOW(),
|
|
));
|
|
if (!$t->save())
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function getAttachments($lang=null) {
|
|
$att = $this->attachments;
|
|
if ($lang)
|
|
$att = $att->window(array('lang' => $lang));
|
|
return $att;
|
|
}
|
|
|
|
function delete() {
|
|
try {
|
|
parent::delete();
|
|
$type = array('type' => 'deleted');
|
|
Signal::send('object.deleted', $this, $type);
|
|
// Cleanup help topics.
|
|
$this->topics->expunge();
|
|
// Cleanup attachments.
|
|
$this->attachments->deleteAll();
|
|
}
|
|
catch (OrmException $ex) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
/* ------------------> Static methods <--------------------- */
|
|
|
|
static function add($vars, &$errors) {
|
|
if(!($faq = self::create($vars)))
|
|
return false;
|
|
|
|
return $faq;
|
|
}
|
|
|
|
static function create($vars=false) {
|
|
$faq = new static($vars);
|
|
$faq->created = SqlFunction::NOW();
|
|
return $faq;
|
|
}
|
|
|
|
static function allPublic() {
|
|
return static::objects()->exclude(Q::any(array(
|
|
'ispublished'=>self::VISIBILITY_PRIVATE,
|
|
'category__ispublic'=>Category::VISIBILITY_PRIVATE,
|
|
)));
|
|
}
|
|
|
|
static function countPublishedFAQs() {
|
|
static $count;
|
|
if (!isset($count)) {
|
|
$count = self::allPublic()->count();
|
|
}
|
|
return $count;
|
|
}
|
|
|
|
static function getFeatured() {
|
|
return self::objects()
|
|
->filter(array('ispublished__in'=>array(1,2), 'category__ispublic'=>1))
|
|
->order_by('-ispublished');
|
|
}
|
|
|
|
static function findIdByQuestion($question) {
|
|
$row = self::objects()->filter(array(
|
|
'question'=>$question
|
|
))->values_flat('faq_id')->first();
|
|
|
|
return ($row) ? $row[0] : null;
|
|
}
|
|
|
|
static function findByQuestion($question) {
|
|
return self::objects()->filter(array(
|
|
'question'=>$question
|
|
))->one();
|
|
}
|
|
|
|
function update($vars, &$errors) {
|
|
global $cfg;
|
|
|
|
// Cleanup.
|
|
$vars['question'] = Format::striptags(trim($vars['question']));
|
|
|
|
// Validate
|
|
if ($vars['id'] && $this->getId() != $vars['id'])
|
|
$errors['err'] = __('Internal error occurred');
|
|
elseif (!$vars['question'])
|
|
$errors['question'] = __('Question required');
|
|
elseif (($qid=self::findIdByQuestion($vars['question'])) && $qid != $vars['id'])
|
|
$errors['question'] = __('Question already exists');
|
|
|
|
if (!$vars['category_id'] || !($category=Category::lookup($vars['category_id'])))
|
|
$errors['category_id'] = __('Category is required');
|
|
|
|
if (!$vars['answer'])
|
|
$errors['answer'] = __('FAQ answer is required');
|
|
|
|
if ($errors)
|
|
return false;
|
|
|
|
$this->question = $vars['question'];
|
|
$this->answer = Format::sanitize($vars['answer']);
|
|
$this->category = $category;
|
|
$this->ispublished = $vars['ispublished'];
|
|
$this->notes = Format::sanitize($vars['notes']);
|
|
$this->keywords = ' ';
|
|
|
|
if (!$this->save())
|
|
return false;
|
|
|
|
$this->updateTopics($vars['topics']);
|
|
|
|
// General attachments (for all languages)
|
|
// ---------------------
|
|
// Delete removed attachments.
|
|
if (isset($vars['files'])) {
|
|
$this->getAttachments()->keepOnlyFileIds($vars['files'], false);
|
|
}
|
|
|
|
$images = Draft::getAttachmentIds($vars['answer']);
|
|
$images = array_flip(array_map(function($i) { return $i['id']; }, $images));
|
|
$this->getAttachments()->keepOnlyFileIds($images, true);
|
|
|
|
// Handle language-specific attachments
|
|
// ----------------------
|
|
$langs = $cfg ? $cfg->getSecondaryLanguages() : false;
|
|
if ($langs) {
|
|
$langs[] = $cfg->getPrimaryLanguage();
|
|
foreach ($langs as $lang) {
|
|
if (!isset($vars['files_'.$lang]))
|
|
// Not updating the FAQ
|
|
continue;
|
|
|
|
$keepers = $vars['files_'.$lang];
|
|
|
|
// FIXME: Include inline images in translated content
|
|
|
|
$this->getAttachments($lang)->keepOnlyFileIds($keepers, false, $lang);
|
|
}
|
|
}
|
|
|
|
if (isset($vars['trans']) && !$this->saveTranslations($vars))
|
|
return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
function save($refetch=false) {
|
|
if ($this->dirty)
|
|
$this->updated = SqlFunction::NOW();
|
|
return parent::save($refetch || $this->dirty);
|
|
}
|
|
|
|
static function getPermissions() {
|
|
return self::$perms;
|
|
}
|
|
}
|
|
|
|
RolePermission::register( /* @trans */ 'Knowledgebase',
|
|
FAQ::getPermissions());
|
|
|
|
class FaqTopic extends VerySimpleModel {
|
|
|
|
static $meta = array(
|
|
'table' => FAQ_TOPIC_TABLE,
|
|
'pk' => array('faq_id', 'topic_id'),
|
|
'select_related' => 'topic',
|
|
'joins' => array(
|
|
'faq' => array(
|
|
'constraint' => array(
|
|
'faq_id' => 'FAQ.faq_id',
|
|
),
|
|
),
|
|
'topic' => array(
|
|
'constraint' => array(
|
|
'topic_id' => 'Topic.topic_id',
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
class FaqAccessMgmtForm
|
|
extends AbstractForm {
|
|
function buildFields() {
|
|
return array(
|
|
'ispublished' => new ChoiceField(array(
|
|
'label' => __('Listing Type'),
|
|
'choices' => array(
|
|
FAQ::VISIBILITY_PRIVATE => __('Internal'),
|
|
FAQ::VISIBILITY_PUBLIC => __('Public'),
|
|
FAQ::VISIBILITY_FEATURED => __('Featured'),
|
|
),
|
|
)),
|
|
);
|
|
}
|
|
}
|