diff --git a/include/class.attachment.php b/include/class.attachment.php index 3247d06912d8baafaa895843556b6236f809a35d..c4fc059c453f8e6da6541b7ff28548d104387393 100644 --- a/include/class.attachment.php +++ b/include/class.attachment.php @@ -90,26 +90,31 @@ class Attachment extends VerySimpleModel { } } -class GenericAttachment extends VerySimpleModel { - static $meta = array( - 'table' => ATTACHMENT_TABLE, - 'pk' => array('id'), - ); -} - -class GenericAttachments { - - var $id; - var $type; - - function GenericAttachments($object_id, $type) { - $this->id = $object_id; - $this->type = $type; +class GenericAttachments +extends InstrumentedList { + + var $lang; + + function getId() { return $this->key['object_id']; } + function getType() { return $this->key['type']; } + + /** + * Drop attachments whose file_id values are not in the included list, + * additionally, add new files whose IDs are in the list provided. + */ + function keepOnlyFileIds($ids, $inline=false, $lang=false) { + $new = array_fill_keys($ids, 1); + foreach ($this as $A) { + $idx = array_search($A->file_id, $ids); + if ($idx === false && (!$A->lang || $A->lang == $lang)) + // Not in the $ids list, delete + $this->remove($A); + unset($new[$A->file_id]); + } + // Everything remaining in $new is truly new + $this->upload(array_keys($new), $inline, $lang); } - function getId() { return $this->id; } - function getType() { return $this->type; } - function upload($files, $inline=false, $lang=false) { $i=array(); if (!is_array($files)) $files=array($files); @@ -125,12 +130,10 @@ class GenericAttachments { $_inline = isset($file['inline']) ? $file['inline'] : $inline; - $att = Attachment::create(array( - 'type' => $this->getType(), - 'object_id' => $this->getId(), + $att = $this->add(Attachment::create(array( 'file_id' => $fileId, 'inline' => $_inline ? 1 : 0, - )); + ))); if ($lang) $att->lang = $lang; @@ -139,31 +142,12 @@ class GenericAttachments { $att->save(); $i[] = $fileId; } - return $i; } function save($file, $inline=true) { - - if (is_numeric($file)) - $fileId = $file; - elseif (is_array($file) && isset($file['id'])) - $fileId = $file['id']; - elseif ($file = AttachmentFile::create($file)) - $fileId = $file->getId(); - else - return false; - - $att = Attachment::create(array( - 'type' => $this->getType(), - 'object_id' => $this->getId(), - 'file_id' => $fileId, - 'inline' => $inline ? 1 : 0, - )); - if (!$att->save()) - return false; - - return $fileId; + $ids = $this->upload($file, $inline); + return $ids[0]; } function getInlines($lang=false) { return $this->_getList(false, true, $lang); } @@ -171,28 +155,30 @@ class GenericAttachments { function getAll($lang=false) { return $this->_getList(true, true, $lang); } function count($lang=false) { return count($this->getSeparates($lang)); } - function _getList($separate=false, $inlines=false, $lang=false) { - return Attachment::objects()->filter(array( - 'type' => $this->getType(), - 'object_id' => $this->getId(), - )); + function _getList($separates=false, $inlines=false, $lang=false) { + $base = $this; + + if ($separates && !$inline) + $base = $base->filter(array('inline' => 0)); + elseif (!$separates && $inline) + $base = $base->filter(array('inline' => 1)); + + if ($lang) + $base = $base->filter(array('lang' => $lang)); + else + $base = $base->filter(array('lang__isnull' => true)); + + return $base; } function delete($file_id) { - return Attachment::objects()->filter(array( - 'type' => $this->getType(), - 'object_id' => $this->getId(), - 'file_id' => $file_id, - ))->delete(); + return $this->objects()->filter(array('file_id'=>$file_id))->delete(); } function deleteAll($inline_only=false){ - $objects = Attachment::objects()->filter(array( - 'type' => $this->getType(), - 'object_id' => $this->getId(), - )); + $objects = $this; if ($inline_only) - $objects->filter(array('inline' => 1)); + $objects = $objects->filter(array('inline' => 1)); return $objects->delete(); } @@ -200,5 +186,12 @@ class GenericAttachments { function deleteInlines() { return $this->deleteAll(true); } + + static function forIdAndType($id, $type) { + return new static(array( + 'Attachment', + array('object_id' => $id, 'type' => $type) + )); + } } ?> diff --git a/include/class.canned.php b/include/class.canned.php index 8c842776af205ac4e66881922b9d32d1d13716ca..9ea3a66cb82bf82b4526cda1c7b2108862b0e3f9 100644 --- a/include/class.canned.php +++ b/include/class.canned.php @@ -34,50 +34,30 @@ class CannedModel { RolePermission::register( /* @trans */ 'Knowledgebase', CannedModel::getPermissions()); -class Canned { - var $id; - var $ht; - - var $attachments; - - function Canned($id){ - $this->id=0; - $this->load($id); - } - - function load($id=0) { - - if(!$id && !($id=$this->getId())) - return false; - - $sql='SELECT canned.*, count(attach.file_id) as attachments ' - .' FROM '.CANNED_TABLE.' canned ' - .' LEFT JOIN '.ATTACHMENT_TABLE.' attach - ON (attach.object_id=canned.canned_id AND attach.`type`=\'C\' AND NOT attach.inline) ' - .' WHERE canned.canned_id='.db_input($id) - .' GROUP BY canned.canned_id'; - - if(!($res=db_query($sql)) || !db_num_rows($res)) - return false; - - - $this->ht = db_fetch_array($res); - $this->id = $this->ht['canned_id']; - $this->attachments = new GenericAttachments($this->id, 'C'); - - return true; - } - - function reload() { - return $this->load(); - } +class Canned +extends VerySimpleModel { + static $meta = array( + 'table' => CANNED_TABLE, + 'pk' => array('canned_id'), + 'joins' => array( + 'attachments' => array( + 'constraint' => array( + "'C'" => 'Attachment.type', + 'canned_id' => 'Attachment.object_id', + ), + 'list' => true, + 'null' => true, + 'broker' => 'GenericAttachments', + ), + ), + ); function getId(){ - return $this->id; + return $this->canned_id; } function isEnabled() { - return ($this->ht['isenabled']); + return $this->isenabled; } function isActive(){ @@ -118,11 +98,11 @@ class Canned { } function getTitle() { - return $this->ht['title']; + return $this->title; } function getResponse() { - return $this->ht['response']; + return $this->response; } function getResponseWithImages() { return Format::viewableImages($this->getResponse()); @@ -202,71 +182,65 @@ class Canned { } function getHashtable() { - return $this->ht; + $base = $this->ht; + unset($base['attachments']); + return $base; } function getInfo() { return $this->getHashtable(); } - function update($vars, &$errors) { - - if(!$this->save($this->getId(),$vars,$errors)) - return false; - - $this->reload(); - - return true; - } - function getNumAttachments() { - return $this->ht['attachments']; + return $this->attachments->count(); } function delete(){ - if ($this->getNumFilters() > 0) return false; + if ($this->getNumFilters() > 0) + return false; - $sql='DELETE FROM '.CANNED_TABLE.' WHERE canned_id='.db_input($this->getId()).' LIMIT 1'; - if(db_query($sql) && ($num=db_affected_rows())) { - $this->attachments->deleteAll(); - } + if (!parent::delete()) + return false; - return $num; + $this->attachments->deleteAll(); + + return true; } /*** Static functions ***/ - function lookup($id){ - return ($id && is_numeric($id) && ($c= new Canned($id)) && $c->getId()==$id)?$c:null; - } - function create($vars,&$errors) { - return self::save(0,$vars,$errors); + static function create($vars=false) { + $faq = parent::create($vars); + $faq->created = SqlFunction::NOW(); + return $faq; } - function getIdByTitle($title) { - $sql='SELECT canned_id FROM '.CANNED_TABLE.' WHERE title='.db_input($title); - if(($res=db_query($sql)) && db_num_rows($res)) - list($id)=db_fetch_row($res); + static function getIdByTitle($title) { + $row = static::objects() + ->filter(array('title' => $title)) + ->values_flat('canned_id') + ->first(); - return $id; + return $row ? $row[0] : null; } - function getCannedResponses($deptId=0, $explicit=false) { - - $sql='SELECT canned_id, title FROM '.CANNED_TABLE - .' WHERE isenabled'; - if($deptId){ - $sql.=' AND (dept_id='.db_input($deptId); - if(!$explicit) - $sql.=' OR dept_id=0'; - $sql.=')'; + static function getCannedResponses($deptId=0, $explicit=false) { + $canned = static::objects() + ->filter(array('isenabled' => true)) + ->order_by('title') + ->values_flat('canned_id', 'title'); + + if ($deptId) { + $depts = Q::any(array('dept_id' => $deptId)); + if (!$explicit) + $depts->add(array('dept_id' => 0)); + $canned->filter($depts); } - $sql.=' ORDER BY title'; $responses = array(); - if(($res=db_query($sql)) && db_num_rows($res)) { - while(list($id,$title)=db_fetch_row($res)) - $responses[$id]=$title; + foreach ($canned as $row) { + list($id, $title) = $row; + $responses[$id] = $title; } return $responses; @@ -276,50 +250,51 @@ class Canned { return self::getCannedResponses($deptId, $explicit); } - function save($id,$vars,&$errors) { + function save($refetch=false) { + if ($this->dirty || $refetch) + $this->updated = SqlFunction::NOW(); + return parent::save($refetch || $this->dirty); + } + + function update($vars,&$errors) { global $cfg; - $vars['title']=Format::striptags(trim($vars['title'])); + $vars['title'] = Format::striptags(trim($vars['title'])); - if($id && $id!=$vars['id']) + $id = isset($this->canned_id) ? $this->canned_id : null; + if ($id && $id != $vars['id']) $errors['err']=__('Internal error. Try again'); - if(!$vars['title']) - $errors['title']=__('Title required'); - elseif(strlen($vars['title'])<3) - $errors['title']=__('Title is too short. 3 chars minimum'); - elseif(($cid=self::getIdByTitle($vars['title'])) && $cid!=$id) - $errors['title']=__('Title already exists'); - - if(!$vars['response']) - $errors['response']=__('Response text is required'); + if (!$vars['title']) + $errors['title'] = __('Title required'); + elseif (strlen($vars['title']) < 3) + $errors['title'] = __('Title is too short. 3 chars minimum'); + elseif (($cid=self::getIdByTitle($vars['title'])) && $cid!=$id) + $errors['title'] = __('Title already exists'); - if($errors) return false; + if (!$vars['response']) + $errors['response'] = __('Response text is required'); - $sql=' updated=NOW() '. - ',dept_id='.db_input($vars['dept_id']?:0). - ',isenabled='.db_input($vars['isenabled']). - ',title='.db_input($vars['title']). - ',response='.db_input(Format::sanitize($vars['response'])). - ',notes='.db_input(Format::sanitize($vars['notes'])); - - if($id) { - $sql='UPDATE '.CANNED_TABLE.' SET '.$sql.' WHERE canned_id='.db_input($id); - if(db_query($sql)) - return true; + if ($errors) + return false; - $errors['err']=sprintf(__('Unable to update %s.'), __('this canned response')); + $this->dept_id = $vars['dept_id'] ?: 0; + $this->isenabled = $vars['isenabled']; + $this->title = $vars['title']; + $this->response = Format::sanitize($vars['response']); + $this->notes = Format::sanitize($vars['notes']); - } else { - $sql='INSERT INTO '.CANNED_TABLE.' SET '.$sql.',created=NOW()'; - if(db_query($sql) && ($id=db_insert_id())) - return $id; + $isnew = !isset($id); + if ($this->save()) + return true; + if ($isnew) + $errors['err'] = sprintf(__('Unable to update %s.'), __('this canned response')); + else $errors['err']=sprintf(__('Unable to create %s.'), __('this canned response')) .' '.__('Internal error occurred'); - } - return false; + return true; } } ?> diff --git a/include/class.draft.php b/include/class.draft.php index 1938064f8bfe5dd605bfc1c56077f0be58e06bd1..d83a47e4e7e456c97b6d0a5a072d73e2a59c1eaa 100644 --- a/include/class.draft.php +++ b/include/class.draft.php @@ -21,16 +21,19 @@ class Draft extends VerySimpleModel { static $meta = array( 'table' => DRAFT_TABLE, 'pk' => array('id'), + 'joins' => array( + 'attachments' => array( + 'constraint' => array( + "'D'" => 'Attachment.type', + 'id' => 'Attachment.object_id', + ), + 'list' => true, + 'null' => true, + 'broker' => 'GenericAttachments', + ), + ), ); - var $attachments; - - function __construct() { - call_user_func_array(array('parent', '__construct'), func_get_args()); - if (isset($this->id)) - $this->attachments = new GenericAttachments($this->id, 'D'); - } - function getId() { return $this->id; } function getBody() { return $this->body; } function getStaffId() { return $this->staff_id; } diff --git a/include/class.faq.php b/include/class.faq.php index f2d42355bff78ea3781f62a4b1ca4b9b8e663421..f3d9d5bb76cfde2e0d454387b153d12475ade707 100644 --- a/include/class.faq.php +++ b/include/class.faq.php @@ -32,11 +32,12 @@ class FAQ extends VerySimpleModel { ), 'attachments' => array( 'constraint' => array( - "'F'" => 'GenericAttachment.type', - 'faq_id' => 'GenericAttachment.object_id', + "'F'" => 'Attachment.type', + 'faq_id' => 'Attachment.object_id', ), 'list' => true, 'null' => true, + 'broker' => 'GenericAttachments', ), 'topics' => array( 'constraint' => array( @@ -57,24 +58,20 @@ class FAQ extends VerySimpleModel { 'primary' => true, )); - var $attachments; var $topics; var $_local; + var $_attachments; const VISIBILITY_PRIVATE = 0; const VISIBILITY_PUBLIC = 1; const VISIBILITY_FEATURED = 2; - function __onload() { - if (isset($this->faq_id)) - $this->attachments = new GenericAttachments($this->getId(), 'F'); - } - /* ------------------> 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; } @@ -148,10 +145,6 @@ class FAQ extends VerySimpleModel { function setKeywords($words) { $this->keywords = $words; } function setNotes($text) { $this->notes = $text; } - /* For ->attach() and ->detach(), use $this->attachments() (nolint) */ - function attach($file) { return $this->attachments->add($file); } - function detach($file) { return $this->attachments->remove($file); } - function publish() { $this->setPublished(1); return $this->save(); @@ -169,7 +162,7 @@ class FAQ extends VerySimpleModel { function printPdf() { global $thisstaff; - require_once(INCLUDE_DIR.'mpdf/mpdf.php'); + require_once(INCLUDE_DIR.'class.pdf.php'); $paper = 'Letter'; if ($thisstaff) @@ -180,7 +173,7 @@ class FAQ extends VerySimpleModel { include STAFFINC_DIR . 'templates/faq-print.tmpl.php'; $html = ob_get_clean(); - $pdf = new mPDF('', $paper); + $pdf = new mPDFWithLocalImages('', $paper); // Setup HTML writing and load default thread stylesheet $pdf->WriteHtml( '<style> @@ -190,7 +183,7 @@ class FAQ extends VerySimpleModel { .thread-body { font-family: serif; }' .file_get_contents(ROOT_DIR.'css/thread.css') .'</style>' - .'<div>'.$html.'</div>'); + .'<div>'.$html.'</div>', 0, true, true); $pdf->Output(Format::slugify($faq->getQuestion()) . '.pdf', 'I'); } @@ -215,9 +208,12 @@ class FAQ extends VerySimpleModel { function getLocalQuestion($lang=false) { return $this->_getLocal('question', $lang); } - function getLocalAnswerWithImages($lang=false) { + function getLocalAnswer($lang=false) { return $this->_getLocal('answer', $lang); } + function getLocalAnswerWithImages($lang=false) { + return Format::viewableImages($this->getLocalAnswer($lang)); + } function _getLocal($what, $lang=false) { if (!$lang) { $lang = $this->getDisplayLang(); @@ -241,8 +237,10 @@ class FAQ extends VerySimpleModel { } function getLocalAttachments($lang=false) { - return $this->attachments->getSeparates( - $lang ?: $this->getDisplayLang()); + return $this->attachments->getSeparates()->filter(Q::any(array( + 'lang__isnull' => true, + 'lang' => $lang ?: $this->getDisplayLang(), + ))); } function updateTopics($ids){ @@ -314,16 +312,18 @@ class FAQ extends VerySimpleModel { return true; } - function getVisibleAttachments() { - return array_merge( - $this->attachments->getSeparates()->all() ?: array(), - $this->getLocalAttachments()->all()); + function getAttachments($lang=false) { + $att = $this->attachments; + if ($lang) + $att = $att->window(array('lang'=>$lang)); + + return $att; } function getAttachmentsLinks($separator=' ',$target='') { $str=''; - if ($attachments = $this->getVisibleAttachments()) { + if ($attachments = $this->getLocalAttachments()->all()) { foreach($attachments as $attachment ) { /* The h key must match validation in file.php */ if($attachment['size']) @@ -441,15 +441,15 @@ class FAQ extends VerySimpleModel { // Delete removed attachments. if (isset($vars['files'])) { $keepers = $vars['files']; - if (($attachments = $this->attachments->getSeparates())) { - foreach($attachments as $file) { - if($file['id'] && !in_array($file['id'], $keepers)) - $this->attachments->delete($file['id']); - } - } } - // Upload new attachments IF any. - $this->attachments->upload($keepers); + else { + $keepers = array(); + } + + $images = Draft::getAttachmentIds($vars['answer']); + $images = array_map(function($i) { return $i['id']; }, $images); + $keepers = array_merge($keepers, $images); + $this->getAttachments()->keepOnlyFileIds($keepers); // Handle language-specific attachments // ---------------------- @@ -463,22 +463,12 @@ class FAQ extends VerySimpleModel { $keepers = $vars['files_'.$lang]; - // Delete removed attachments. - if (($attachments = $this->attachments->getSeparates($lang))) { - foreach ($attachments as $file) { - if ($file['id'] && !in_array($file['id'], $keepers)) - $this->attachments->delete($file['id']); - } - } - // Upload new attachments IF any. - $this->attachments->upload($keepers, false, $lang); + // FIXME: Include inline images in translated content + + $this->getAttachments()->keepOnlyFileIds($keepers, false, $lang); } } - // Inline images (attached to the draft) - $this->attachments->deleteInlines(); - $this->attachments->upload(Draft::getAttachmentIds($vars['answer'])); - if (isset($vars['trans']) && !$this->saveTranslations($vars)) return false; diff --git a/include/class.forms.php b/include/class.forms.php index 38cb31ef7639550239107deea6c818599ab9501b..f1745be92f6e830d5c06fab30c5c641da1e0db22 100644 --- a/include/class.forms.php +++ b/include/class.forms.php @@ -119,6 +119,9 @@ class Form { if (!$field->hasData()) continue; + // Prefer indexing by field.id if indexing numerically + if (is_int($key) && $field->get('id')) + $key = $field->get('id'); $this->_clean[$key] = $this->_clean[$field->get('name')] = $field->getClean(); } @@ -2233,7 +2236,7 @@ class FileUploadField extends FormField { if (!isset($this->attachments) && ($a = $this->getAnswer()) && ($e = $a->getEntry()) && ($e->get('id')) ) { - $this->attachments = new GenericAttachments( + $this->attachments = GenericAttachments::forIdAndType( // Combine the field and entry ids to make the key sprintf('%u', crc32('E'.$this->get('id').$e->get('id'))), 'E'); @@ -2304,19 +2307,7 @@ class FileUploadField extends FormField { function to_database($value) { $this->getFiles(); if (isset($this->attachments)) { - $ids = array(); - // Handle deletes - foreach ($this->attachments->getAll() as $f) { - if (!in_array($f->id, $value)) - $this->attachments->delete($f->id); - else - $ids[] = $f->id; - } - // Handle new files - foreach ($value as $id) { - if (!in_array($id, $ids)) - $this->attachments->upload($id); - } + $this->attachments->keepOnlyFileIds($value); } return JsonDataEncoder::encode($value); } @@ -3037,19 +3028,26 @@ class FileUploadWidget extends Widget { $mimetypes = array_filter($config['__mimetypes'], function($t) { return strpos($t, '/') !== false; } ); - $files = array(); - foreach ($this->value ?: array() as $fid) { - $found = false; - foreach ($attachments as $f) { - $file = $f->file; - $files[] = array( - 'id' => $file->getId(), - 'name' => $file->getName(), - 'type' => $file->getType(), - 'size' => $file->getSize(), - 'download_url' => $file->getDownloadUrl(), - ); - } + $files = $F = array(); + $new = array_fill_keys($this->field->getClean(), 1); + foreach ($attachments as $f) { + $F[] = $f->file; + unset($new[$file->id]); + } + // Add in newly added files not yet saved (if redisplaying after an + // error) + if ($new) { + $F = array_merge($F, AttachmentFile::objects()->filter(array( + 'id__in' => array_keys($new)))->all()); + } + foreach ($F as $file) { + $files[] = array( + 'id' => $file->getId(), + 'name' => $file->getName(), + 'type' => $file->getType(), + 'size' => $file->getSize(), + 'download_url' => $file->getDownloadUrl(), + ); } ?><div id="<?php echo $id; ?>" class="filedrop"><div class="files"></div> @@ -3081,10 +3079,9 @@ class FileUploadWidget extends Widget { } function getValue() { - $data = $this->field->getSource(); - $ids = array(); // Handle manual uploads (IE<10) if ($_SERVER['REQUEST_METHOD'] == 'POST' && isset($_FILES[$this->name])) { + $ids = array(); foreach (AttachmentFile::format($_FILES[$this->name]) as $file) { try { $F = $this->field->uploadFile($file); @@ -3095,10 +3092,7 @@ class FileUploadWidget extends Widget { return array_merge($ids, parent::getValue() ?: array()); } // If no value was sent, assume an empty list - elseif ($data && is_array($data) && !isset($data[$this->name])) - return array(); - - return parent::getValue(); + return parent::getValue() ?: array(); } } diff --git a/include/class.i18n.php b/include/class.i18n.php index 5d5000b2a9b06bafbdad7025ea918f2308d7560e..bc7634329ea581a81727b0cf80c80a1a490b1f7f 100644 --- a/include/class.i18n.php +++ b/include/class.i18n.php @@ -146,12 +146,9 @@ class Internationalization { if (($tpl = $this->getTemplate('templates/premade.yaml')) && ($canned = $tpl->getData())) { foreach ($canned as $c) { - if (($id = Canned::create($c, $errors)) + if (($premade = Canned::create($c)) && isset($c['attachments'])) { - $premade = Canned::lookup($id); - foreach ($c['attachments'] as $a) { - $premade->attachments->save($a, false); - } + $premade->attachments->upload($c['attachments']); } } } diff --git a/include/class.orm.php b/include/class.orm.php index 39ad7728b7f8030226c1e97379f9b59a45acc419..326736f81012d171fd0c9f502025aec956f9b157 100644 --- a/include/class.orm.php +++ b/include/class.orm.php @@ -96,6 +96,12 @@ class ModelMeta implements ArrayAccess { = explode('.', $foreign); } } + if ($j['list'] && !isset($j['broker'])) { + $j['broker'] = 'InstrumentedList'; + } + if ($j['broker'] && !class_exists($j['broker'])) { + throw new OrmException($j['broker'] . ': List broker does not exist'); + } foreach ($constraint as $local => $foreign) { list($class, $field) = $foreign; if ($local[0] == "'" || $field[0] == "'" || !class_exists($class)) @@ -193,7 +199,7 @@ class VerySimpleModel { $fkey[$F ?: $_klas] = ($local[0] == "'") ? trim($local, "'") : $this->ht[$local]; } - $v = $this->ht[$field] = new InstrumentedList( + $v = $this->ht[$field] = new $j['broker']( // Send Model, [Foriegn-Field => Local-Id] array($class, $fkey) ); @@ -1381,7 +1387,11 @@ class InstrumentedList extends ModelInstanceManager { function add($object, $at=false) { if (!$object || !$object instanceof $this->model) - throw new Exception(__('Attempting to add invalid object to list')); + throw new Exception(sprintf( + 'Attempting to add invalid object to list. Expected <%s>, but got <%s>', + $this->model, + get_class($object) + )); foreach ($this->key as $field=>$value) $object->set($field, $value); @@ -1407,6 +1417,23 @@ class InstrumentedList extends ModelInstanceManager { $this->cache = array(); } + /** + * Reduce the list to a subset using a simply key/value constraint. New + * items added to the subset will have the constraint automatically + * added to all new items. + */ + function window($constraint) { + $model = $this->model; + $meta = $model::$meta; + $key = $this->key; + foreach ($constraint as $field=>$value) { + if (!is_string($field) || false === in_array($field, $meta['fields'])) + throw new OrmException('InstrumentedList windowing must be performed on local fields only'); + $key[$field] = $value; + } + return new static(array($this->model, $key), $this->filter($constraint)); + } + // QuerySet delegates function count() { return $this->objects()->count(); diff --git a/include/class.page.php b/include/class.page.php index a49a7b263d12632ee662aba4c1288c2bcf4616b9..69596e88257fb00a167af3f7b99897ced071b10c 100644 --- a/include/class.page.php +++ b/include/class.page.php @@ -24,16 +24,20 @@ class Page extends VerySimpleModel { 'topics' => array( 'reverse' => 'Topic.page', ), + 'attachments' => array( + 'constraint' => array( + "'P'" => 'Attachment.type', + 'id' => 'Attachment.object_id', + ), + 'list' => true, + 'null' => true, + 'broker' => 'GenericAttachments', + ), ), ); - var $attachments; var $_local; - function __onload() { - $this->attachments = new GenericAttachments($this->id, 'P'); - } - function getId() { return $this->id; } @@ -285,9 +289,9 @@ class Page extends VerySimpleModel { $rv = $this->saveTranslations($vars, $errors); // Attach inline attachments from the editor - $this->attachments->deleteInlines(); - $this->attachments->upload( - Draft::getAttachmentIds($vars['body']), true); + $keepers = Draft::getAttachmentIds($vars['body']); + $keepers = array_map(function($i) { return $i['id']; }, $keepers); + $this->attachments->keepOnlyFileIds($keepers, true); if ($rv) return $rv; diff --git a/include/class.pdf.php b/include/class.pdf.php index 32bc4b8810ecb66d5f9b4cbadea0a164ff7ed790..20006d01ec6788b1ad9318a12166212882420b6b 100644 --- a/include/class.pdf.php +++ b/include/class.pdf.php @@ -18,7 +18,36 @@ define('THIS_DIR', str_replace('\\', '/', Misc::realpath(dirname(__FILE__))) . ' require_once(INCLUDE_DIR.'mpdf/mpdf.php'); -class Ticket2PDF extends mPDF +class mPDFWithLocalImages extends mPDF { + function WriteHtml($html) { + static $filenumber = 1; + $args = func_get_args(); + $self = $this; + $images = $cids = array(); + // Try and get information for all the files in one query + if (preg_match_all('/"cid:([\w._-]{32})"/', $html, $cids)) { + foreach (AttachmentFile::objects() + ->filter(array('key__in' => $cids[1])) + as $file + ) { + $images[strtolower($file->getKey())] = $file; + } + } + $args[0] = preg_replace_callback('/"cid:([\w.-]{32})"/', + function($match) use ($self, $images, &$filenumber) { + if (!($file = @$images[strtolower($match[1])])) + return $match[0]; + $key = "__attached_file_".$filenumber++; + $self->{$key} = $file->getData(); + return 'var:'.$key; + }, + $html + ); + return call_user_func_array(array('parent', 'WriteHtml'), $args); + } +} + +class Ticket2PDF extends mPDFWithLocalImages { var $includenotes = false; @@ -42,24 +71,6 @@ class Ticket2PDF extends mPDF return $this->ticket; } - function WriteHtml() { - static $filenumber = 1; - $args = func_get_args(); - $text = &$args[0]; - $self = $this; - $text = preg_replace_callback('/cid:([\w.-]{32})/', - function($match) use ($self, &$filenumber) { - if (!($file = AttachmentFile::lookup($match[1]))) - return $match[0]; - $key = "__attached_file_".$filenumber++; - $self->{$key} = $file->getData(); - return 'var:'.$key; - }, - $text - ); - call_user_func_array(array('parent', 'WriteHtml'), $args); - } - function _print() { global $thisstaff, $thisclient, $cfg; diff --git a/include/class.template.php b/include/class.template.php index c1bdb80df2135a6b81c09a33c6d7973dbd03c5a4..18c04548731719bcc09a169d50f0d639dc437a8c 100644 --- a/include/class.template.php +++ b/include/class.template.php @@ -507,7 +507,7 @@ class EmailTemplate { $this->ht=db_fetch_array($res); $this->id=$this->ht['id']; - $this->attachments = new GenericAttachments($this->id, 'T'); + $this->attachments = GenericAttachments::forIdAndType($this->id, 'T'); return true; } @@ -586,12 +586,10 @@ class EmailTemplate { $this->reload(); // Inline images (attached to the draft) - if (isset($vars['draft_id']) && $vars['draft_id']) { - if ($draft = Draft::lookup($vars['draft_id'])) { - $this->attachments->deleteInlines(); - $this->attachments->upload($draft->getAttachmentIds($this->getBody()), true); - } - } + $keepers = Draft::getAttachmentIds($this->getBody()); + // Just keep the IDs only + $keepers = array_map(function($i) { return $i['id']; }, $keepers); + $this->attachments->keepOnlyFileIds($keepers, true); return true; } diff --git a/include/client/faq.inc.php b/include/client/faq.inc.php index b879645c8f54d92ccc6b6ab99538d344d80564b5..36c25d5cfe2912d52dfb8984dbd15dca7c80aaf1 100644 --- a/include/client/faq.inc.php +++ b/include/client/faq.inc.php @@ -17,10 +17,11 @@ $category=$faq->getCategory(); <div class="article-title flush-left"> <?php echo $faq->getLocalQuestion() ?> </div> -<div class="faded"><?php echo __('Last updated').' '.Format::daydatetime($category->getUpdateDate()); ?></div> +<div class="faded"><?php echo __('Last updated').' ' + . Format::relativeTime(Misc::db2gmtime($category->getUpdateDate())); ?></div> <br/> <div class="thread-body bleed"> -<?php echo Format::safe_html($faq->getLocalAnswerWithImages()); ?> +<?php echo $faq->getLocalAnswerWithImages(); ?> </div> </div> </div> @@ -35,8 +36,8 @@ $category=$faq->getCategory(); <input type="submit" style="display:none" value="search"/> </form> </div> -<div class="content"> -<?php if ($attachments = $faq->getVisibleAttachments()) { ?> +<div class="content"><?php + if ($attachments = $faq->getLocalAttachments()->all()) { ?> <section> <strong><?php echo __('Attachments');?>:</strong> <?php foreach ($attachments as $att) { ?> @@ -48,17 +49,16 @@ $category=$faq->getCategory(); </div> <?php } ?> </section> -<?php } ?> - -<?php if ($faq->getHelpTopics()->count()) { ?> +<?php } +if ($faq->getHelpTopics()->count()) { ?> <section> <strong><?php echo __('Help Topics'); ?></strong> <?php foreach ($faq->getHelpTopics() as $topic) { ?> <div><?php echo $topic->getFullName(); ?></div> <?php } ?> </section> -<?php } ?> -</div> +<?php } +?></div> </div> </div> diff --git a/include/staff/cannedresponse.inc.php b/include/staff/cannedresponse.inc.php index 4679c14c0fb1aab857019f0bc0c63a1fa45be717..930052225625746cc9b088d608e11407f85ee2f0 100644 --- a/include/staff/cannedresponse.inc.php +++ b/include/staff/cannedresponse.inc.php @@ -93,12 +93,8 @@ $info=Format::htmlchars(($errors && $_POST)?$_POST:$info); </div> <?php $attachments = $canned_form->getField('attachments'); - if ($canned) { + if ($canned && $attachments) { $attachments->setAttachments($canned->attachments); - if ($files = $canned->getAttachedFiles()) { - $ids = $files->values_flat('id')->all(); - $attachments->value = $ids; - } } print $attachments->render(); ?> <br/> diff --git a/include/staff/faq-categories.inc.php b/include/staff/faq-categories.inc.php index 5e55d7ff69a6d8d86a29d8bda2c628896dfdf65e..d1b7316a8dcaa06c01bae05805a766790e9d961d 100644 --- a/include/staff/faq-categories.inc.php +++ b/include/staff/faq-categories.inc.php @@ -15,7 +15,6 @@ if(!defined('OSTSTAFFINC') || !$thisstaff) die('Access Denied'); ->annotate(array('faq_count'=>SqlAggregate::COUNT('faqs'))) ->filter(array('faq_count__gt'=>0)) ->order_by('name'); -print $categories; foreach ($categories as $C) { echo sprintf('<option value="%d" %s>%s (%d)</option>', $C->getId(), diff --git a/include/staff/faq-view.inc.php b/include/staff/faq-view.inc.php index 8afda9a67e320f6636f9fe4119c011561ea35a58..05a6b0f48cf50d15087677dcb286707f0003c944 100644 --- a/include/staff/faq-view.inc.php +++ b/include/staff/faq-view.inc.php @@ -12,14 +12,14 @@ $category=$faq->getCategory(); </div> <div class="pull-right sidebar faq-meta"> -<?php if ($attachments = $faq->getVisibleAttachments()) { ?> +<?php if ($attachments = $faq->getLocalAttachments()->all()) { ?> <section> <strong><?php echo __('Attachments');?>:</strong> <?php foreach ($attachments as $att) { ?> <div> - <a href="file.php?h=<?php echo $att['download']; ?>" class="no-pjax"> + <a href="<?php echo $att->file->getDownloadUrl(); ?>" class="no-pjax"> <i class="icon-file"></i> - <?php echo Format::htmlchars($att['name']); ?> + <?php echo Format::htmlchars($att->file->name); ?> </a> </div> <?php } ?> @@ -67,24 +67,22 @@ if ($otherLangs) { ?> <div class="faq-content"> <div class="faq-manage pull-right"> - <button> - <i class="icon-print"></i> <?php $query = array(); parse_str($_SERVER['QUERY_STRING'], $query); $query['a'] = 'print'; $query['id'] = $faq->getId(); $query = http_build_query($query); ?> - <a href="faq.php?<?php echo $query; ?>" class="no-pjax"><?php - echo __('Print'); ?> - </a></button> + <a href="faq.php?<?php echo $query; ?>" class="no-pjax action-button"> + <i class="icon-print"></i> + <?php echo __('Print'); ?> + </a> <?php if ($thisstaff->getRole()->hasPerm(FAQ::PERM_MANAGE)) { ?> - <button> + <a href="faq.php?id=<?php echo $faq->getId(); ?>&a=edit" class="action-button"> <i class="icon-edit"></i> - <a href="faq.php?id=<?php echo $faq->getId(); ?>&a=edit"><?php - echo __('Edit FAQ'); ?> - </a></button> + <?php echo __('Edit FAQ'); ?> + </a> <?php } ?> </div> @@ -92,7 +90,7 @@ if ($thisstaff->getRole()->hasPerm(FAQ::PERM_MANAGE)) { ?> </div> <div class="faded"><?php echo __('Last updated');?> - <?php echo Format::daydatetime($category->getUpdateDate()); ?> + <?php echo Format::relativeTime(Misc::db2gmtime($category->getUpdateDate())); ?> </div> <br/> <div class="thread-body bleed"> diff --git a/include/staff/login.header.php b/include/staff/login.header.php index 7cf18d895239f5a9425428cb112d6a1aba1827a1..d4068027840b811e2641f4870e6cd3927603e4f9 100644 --- a/include/staff/login.header.php +++ b/include/staff/login.header.php @@ -13,7 +13,7 @@ defined('OSTSCPINC') or die('Invalid path'); <meta http-equiv="cache-control" content="no-cache" /> <meta http-equiv="pragma" content="no-cache" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=0"> - <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-1.8.3.min.js"></script> + <script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-1.11.2.min.js"></script> <script type="text/javascript"> $(document).ready(function() { $("input:not(.dp):visible:enabled:first").focus(); diff --git a/include/staff/templates/faq-print.tmpl.php b/include/staff/templates/faq-print.tmpl.php index 76842eeb37dec37a27f1695f570ecedfc93f761f..6f07f898dbd0c00510b2af5e63975c1c9c095e59 100644 --- a/include/staff/templates/faq-print.tmpl.php +++ b/include/staff/templates/faq-print.tmpl.php @@ -8,5 +8,5 @@ <br/> <div class="thread-body bleed"> -<?php echo $faq->getLocalAnswerWithImages(); ?> +<?php echo $faq->getLocalAnswer(); ?> </div> diff --git a/scp/canned.php b/scp/canned.php index 129d1993bea8870d09585dbb98e9900a7d0ff2ad..85af1b2c46e4420a0e7528f95a33e76d8ab96060 100644 --- a/scp/canned.php +++ b/scp/canned.php @@ -48,27 +48,17 @@ if ($_POST) { //Delete removed attachments. //XXX: files[] shouldn't be changed under any circumstances. $keepers = $canned_form->getField('attachments')->getClean(); - $attachments = $canned->attachments->getSeparates(); //current list of attachments. - foreach($attachments as $k=>$file) { - if(isset($file->id) && !in_array($file->id, $keepers)) { - $canned->attachments->delete($file->id); - } - } - - //Upload NEW attachments IF ANY - TODO: validate attachment types?? - if ($keepers) - $canned->attachments->upload($keepers); // Attach inline attachments from the editor if (isset($_POST['draft_id']) && ($draft = Draft::lookup($_POST['draft_id']))) { - $canned->attachments->deleteInlines(); - $canned->attachments->upload( - $draft->getAttachmentIds($_POST['response']), - true); + $images = $draft->getAttachmentIds($_POST['response']); + $images = array_map(function($i) { return $i['id']; }, $images); + $keepers = array_merge($keepers, $images); } - $canned->reload(); + // Upload NEW attachments IF ANY - TODO: validate attachment types?? + $canned->attachments->keepOnlyFileIds($keepers); // XXX: Handle nicely notifying a user that the draft was // deleted | OR | show the draft for the user on the name @@ -83,18 +73,19 @@ if ($_POST) { } break; case 'create': - if(($id=Canned::create($_POST, $errors))) { + $premade = FAQ::create(); + if ($premade->update($_POST,$errors)) { $msg=sprintf(__('Successfully added %s'), Format::htmlchars($_POST['title'])); $_REQUEST['a']=null; //Upload attachments $keepers = $canned_form->getField('attachments')->getClean(); - if (($c=Canned::lookup($id)) && $keepers) - $c->attachments->upload($keepers); + if ($keepers) + $premade->attachments->upload($keepers); // Attach inline attachments from the editor - if ($c && isset($_POST['draft_id']) + if (isset($_POST['draft_id']) && ($draft = Draft::lookup($_POST['draft_id']))) - $c->attachments->upload( + $premade->attachments->upload( $draft->getAttachmentIds($_POST['response']), true); // Delete this user's drafts for new canned-responses diff --git a/scp/faq.php b/scp/faq.php index 09b0d6bcb04a0b1cb0c9cfca40786b4c7b26625d..bac34c7ceefe4b8cd1c30bd396c93cab2745bc02 100644 --- a/scp/faq.php +++ b/scp/faq.php @@ -130,23 +130,13 @@ else { // Multi-lingual system foreach ($langs as $lang) { $attachments = $faq_form->getField('attachments.'.$lang); - if ($files = $faq->attachments->getSeparates($lang)) { - $ids = array(); - foreach ($files as $f) - $ids[] = $f['id']; - $attachments->value = $ids; - } + $attachments->setAttachments($faq->getAttachments($lang)); } } if ($faq) { // Common attachments $attachments = $faq_form->getField('attachments'); - if ($files = $faq->attachments->getSeparates()) { - $ids = array(); - foreach ($files as $f) - $ids[] = $f['id']; - $attachments->value = $ids; - } + $attachments->setAttachments($faq->getAttachments()); } } diff --git a/scp/js/scp.js b/scp/js/scp.js index 437ce5f6f3f2d59317ed1b3621a42bfa6879a095..e356e825e7810bdf1c92891efa61af38a6f58cde 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -632,7 +632,10 @@ $.dialog = function (url, codes, cb, options) { $('div.body', $popup).slideDown({ duration: 300, queue: false, - complete: function() { if (options.onshow) options.onshow(); } + complete: function() { + if (options.onshow) options.onshow(); + $(this).removeAttr('style'); + } }); $("input[autofocus]:visible:enabled:first", $popup).focus(); var submit_button = null;