Newer
Older
<?php
/*********************************************************************
class.faq.php
Backend support for article creates, edits, deletes, and attachments.
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',
'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,
));
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']);
function getKeywords() { return $this->keywords; }
function getQuestion() { return $this->question; }
function getAnswer() { return $this->answer; }
return Format::viewableImages($this->answer, ['type' => 'F']);
function getTeaser() {
return Format::truncate(Format::striptags($this->answer), 150);
}
return ThreadEntryBody::fromFormattedText($this->answer, 'html')
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; }
foreach ($this->getHelpTopics() as $T)
$ids[] = $T->topic->getId();
function getHelpTopicNames() {
$names = array();
foreach ($this->getHelpTopics() as $T)
$names[] = $T->topic->getFullName();
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);
}
function unpublish() {
$this->setPublished(0);
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('', $paper);
// 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;
try {
parent::delete();
// Cleanup help topics.
// Cleanup attachments.
$this->attachments->deleteAll();
}
catch (OrmException $ex) {
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(
))->values_flat('faq_id')->first();
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');
$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->updateTopics($vars['topics']);
if (!$this->save())
return false;
// 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_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'),
),
)),
);
}
}