diff --git a/README.md b/README.md index ccca57b02ea094d7eb01e04522f972eeae7fc58c..e231d24008f930eb0fead31fabb74006b27a0c24 100644 --- a/README.md +++ b/README.md @@ -37,12 +37,12 @@ install the software and track updates is to clone the public repository. Create a folder on you web server (using whatever method makes sense for you) and cd into it. Then clone the repository (the folder must be empty!): - git clone https://github.com/osTicket/osTicket-1.8 . + git clone https://github.com/osTicket/osTicket And deploy the code into somewhere in your server's www root folder, for instance - cd osTicket-1.8 + cd osTicket php manage.php deploy --setup /var/www/htdocs/osticket/ Then you can configure your server if necessary to serve that folder, and @@ -60,8 +60,8 @@ osTicket supports upgrading from 1.6-rc1 and later versions. As with any upgrade, strongly consider a backup of your attachment files, database, and osTicket codebase before embarking on an upgrade. -To trigger the update process, fetch the osTicket-1.8 tarball from either -the osTicket [github](http://github.com/osTicket/osTicket-1.8/releases) page +To trigger the update process, fetch the osTicket tarball from either +the osTicket [github](http://github.com/osTicket/osTicket/releases) page or from the [osTicket website](http://osticket.com). Extract the tarball into the folder of your osTicket codebase. This can also be accomplished with the zip file, and a FTP client can of course be used to upload the new diff --git a/WHATSNEW.md b/WHATSNEW.md index 4812a8892ca4cfa988bb47fe87d3a42362463f16..b7162b2285383954e7fa8bae4159b4a83f0d3772 100644 --- a/WHATSNEW.md +++ b/WHATSNEW.md @@ -1,5 +1,166 @@ +osTicket v1.10-rc.3 +=================== +### Enhancements + * Compatibility with PHP7 (#2828) + * Share tickets among organization members (#2405) + * Add lock semantics compatible with v1.9 (lock on view) (f826189) + * Staff login backdrop is customizable (#2468) + * Add advanced search for closed date, thread last message, thread last + response (#2444) + * Disable auto-claim by department (#2591) + * Properly flag SYSTEM thread postings (#2702) + * Add option to use dept/agent name on replies (#2700) + * Add a preference option to set the sort order of the thread entries in DESC + or ASC order (#2700) + * Thread dates can be shown as relative or absolute timestamps (#2700) + * Make Avatars optional on thread view (#2701) + * Make Authentication Tokens Optional (auto-login links in emails) (#2714) + * Use icons for ticket and task actions (#2760) + * role: Add option to use primary role on assignment (#2832) + +### Improvements + * All improvements cited in v1.9.12 and v1.9.13 + * Fix deleting of custom logos (#2433) + * Fix assignment setting on new tasks (#2452) + * Fix subject display of non-short-answer fields on ticket view and ticket + queue (#2463) + * Fix advanced search of ticket source (#2479) + * Forbid adding deleted forms via "Manage Forms" (#2483) + * Use horizontal tabs for translatable article content rather than the left + tabs in a table (#2484) + * Fix lock expiration time if PHP and database have different time zones + (#2533) + * Fix user class and ID matching from email headers (#2549) + * Fix emission of `Content-Language` header in client portal for multiple + system languages, thanks @t-oster (#2555) + * Fix deployment of fresh git repo or download on PHP 5.6 (#2571) + * Fix handling of abbreviated database timezones like `CDT` (#2570) + * Fix incorrect height display of avatars (#2580, #2609) + * Sort help topic names case insensitively, thanks @jdelhome3578 (#2530) + * Fix detection of looped emails (f2cac64) + * Fix crash in ticket preview (popout) if ticket has no thread (bd9e9c5) + * Fix javascript crash adding new ticket filter (d2af0eb) + * Fix crash if the `name` field of a user is a drop-down (ec0b2c5) + * Fix incorrect SQL query removing departments (cf6cd81) + * Properly fallback to database file storage if system is misconfigured (1580136) + * Fix crash handling fields with `__` in the name in the VisibilityConstraint + class (b3d09b6) + * Remove staff-dept records when removing an agent (ecf6931) + * Avoid crashing processing ORM records with NULL select_related models (#2589) + * Fix several full-text search related issues (#2588, #2603) + * Fix crash sending registration link for a guest user (#2552) + * Avoid showing lock icon for expired locks on ticket listing (#2617) + * Fix incorrect redirect from SSO authentication, thanks @kevinoconnor7 + (#2641) + * Fix vertical overflow of uploaded image preview (#2616) + * Fix unnecessary dropping of CDATA table on MySQL 5.6 (#2638) + * Fix several issues on user directory ticket listing (#2626) + * Fix encoding of attachment filenames in emails (#2586) + * Fix warning rendering advanced search dialog, thanks @t-oster (#2594) + * Fix bounce message loop for message alert to a bad agent email address + (#2639) + * Make fulltext search optional on user lookup (#2657) + * Add the [claim] feature again (#2681) + * Fix agent's Signature & Timezone dropped on update (#2720) + * Fix crash in user CSV import (#2708) + * Fix crash in user ajax lookup (#2600) + * Send Reference and In-Reply-To headers only for thread items pertinent to + the receiving user (#2723) + * Properly clean HTML custom fields (#2736) + * Fix changing/saving properties on internal ticket statuses, with the + exception of the state (#2767) + * Fix CSV list import (#2738) + * Fix late redirect header for single ticket typeahead result (#2830) + * Add sortable column headers in the ticket and task queues (#2761) + * Fix several issues with the file CLI app (#2808) + * Fix config crash on install (#2827, #2844) + * Set due date based on user's timezone (#2812, #2981) + * Fix crash rendering some email addresses to string (#2844) + * Fix crash rendering thread with invalid timestamps (#2844) + * Log assignment note (comments), if any, when staff created ticket is + assigned (#2944) + * Change transient SLA, on transfer, if target department has a valid SLA + (#2944) + * Fix typo on task transfer modal dialog (#2944) + * Fix ticket source on ticket edit (#2944) + * Convert user time to database time when querying stats (#2944) + * Fix date picker clearing input on invalid date format (#2944) + * Show topic-specific thank-you page (#2915) + * Department manager can be excluded from the new ticket alert (#2974) + * Do not scrub iframe `@src` attribute (#2940) + +### Performance and Security + * Use full-text search for quick-search typeahead boxes (#2479) + * Speed up a few slow and noisy queries (5c68eb3, 340fee7, 208fcc3) + * Lower memory requirements processing attachments (#2491, #2492) + * Ensure agent still has access when reopening a ticket (#2768) + * Always perform validation server-side for ajax uploads (#2844) + * Protect access to files shown in the FileUpload field (#2618) + * Decode entities prior to HTML scrubbing (#2940) + +### Known Issues + * Uploading multiple files simultaneous (via drag and drop) will cause some + files to be dropped + +osTicket v1.10-rc.2 +=================== +### Enhancements + * Lazy locking system for ticket locking (#2325, #2351, 37cdf25, de92ec5, + 37a0676) + * Add settings for avatars and local "Oscar's A-Team" avatars (#2334) + * Several UI tweaks (7436195, #2426) + * Add transfer and assign mass actions to tickets (#2375) + * Import agents from the command line (#2323) + * User select dialog can be opened after closing in new ticket by staff + (605c313) + * Deadband new message alert and autoresponse to once per five minutes per + user per thread (598dedc) + * [Add Rule] button to add many new rules at one to a ticket filter (c03279d) + +### Improvements + * Fix several install and upgrade-related issues (fc10dcb, e1ca975, b709139, + abc8619, #2411, 832ea94, abb9a08, e3bb6c2, 8e373d4) + * Fix database timezone detection on Windows (#2297) + * Fix several tasks related issues (#2311, #2344, #2376, #2400, #2421, c3d48a9) + * Fix hiding of department-specific canned responses (#2315) + * Fix add and edit of ticket status list items (#2314) + * Fix incorrect definition of some ORM tables (#2324, 69839af) + * Fix crash rendering a closed ticket (#2328) + * Fix case-insensitive sorting of help topics (#2357) + * Fix several advanced search related issues (#2317, 3d4313f, ce3ceae, + b5e6d4e, 5a935ca) + * Fix incorrect SQL deleting a department (#2359) + * Fix incorrect array usage of department members for alerts (#2356) + * Add missing perm for view all agents' stats (#2358) + * Fix missing thread inline images from redactor image manager (be77da4) + * Fix updating configuration for file upload fields (2f4f9c1) + * Fix crash creating tickets with canned attachments (a156bba) + * Fix missing inline images in mailouts (84c9b54) + * Prefer submitted text over last-saved draft (46ab79b) + * Fix incorrect FAQ link in front-page sidebar (ea9dd5f) + * Fix missing assignee selection on new ticket by staff (7865eee) + * Fix issue details showing up on ticket edit (a183a98, 7fbd0f6) + * Fix inability to change SLA on some tickets (#2392) + * Fix auto-claim on new ticket by staff if a filter added a canned reply (c2ce2e9) + * Fix Dept::getMembersForAlerts() missing primary members (abc93efd) + * Fix inability to create tickets if missing the ASSIGN permission on all + depts (0c49e62) + * Fix inability as staff to reset a user's password (0006dd8) + * Render fields marked !visible and !editable, but required on the client + portal (7f55a0b) + * Fix sorting of help topics (a7cc49f, 08a32a4) + * Fix new message alert to a random staff member (d3685a9) + * Fix saving abbreviations on new list items (538087b) + * Fix parsing of some multi-part MIME messages (c57c22a) + * Fix numerous crashes + +### Performance and Security + * Improve performance loading the ticket view (6bba226, 4b12d54) + * Improve performance loading queue statistics (0a89510, 6b76402) + * Dramatically improve full-text search performance (167287d) + osTicket 1.10 -============= +================== ## Major New Features ### Internationalization, Phase III diff --git a/include/ajax.forms.php b/include/ajax.forms.php index f99870a9770d436cc36c028c068a5d25ba9061d9..520e8584a80b6107d4b63af9f7bd5b309694ac54 100644 --- a/include/ajax.forms.php +++ b/include/ajax.forms.php @@ -128,6 +128,20 @@ class DynamicFormsAjaxAPI extends AjaxController { include(STAFFINC_DIR . 'templates/list-items.tmpl.php'); } + function previewListItem($list_id, $item_id) { + + $list = DynamicList::lookup($list_id); + if (!$list) + Http::response(404, 'No such list item'); + + $list = CustomListHandler::forList($list); + if (!($item = $list->getItem( (int) $item_id))) + Http::response(404, 'No such list item'); + + $form = $list->getListItemBasicForm($item->ht, $item); + include(STAFFINC_DIR . 'templates/list-item-preview.tmpl.php'); + } + function saveListItem($list_id, $item_id) { global $thisstaff; diff --git a/include/ajax.thread.php b/include/ajax.thread.php index f53a55182d86b0de6348ea6d5b00d815d2465966..235a5540b09a649a73127976910a1d75ee409986 100644 --- a/include/ajax.thread.php +++ b/include/ajax.thread.php @@ -230,9 +230,13 @@ class ThreadAjaxAPI extends AjaxController { $errors = $info = array(); if ($thread->updateCollaborators($_POST, $errors)) - Http::response(201, sprintf('Recipients (%d of %d)', - $thread->getNumActiveCollaborators(), - $thread->getNumCollaborators())); + Http::response(201, $this->json_encode(array( + 'id' => $thread->getId(), + 'text' => sprintf('Recipients (%d of %d)', + $thread->getNumActiveCollaborators(), + $thread->getNumCollaborators()) + ) + )); if($errors && $errors['err']) $info +=array('error' => $errors['err']); diff --git a/include/class.dynamic_forms.php b/include/class.dynamic_forms.php index 6131a294b747e7328cfe3a1dcfd88cb7ae8a7673..7bb040814714eeaa7b6f4400ef84d3e98391cccf 100644 --- a/include/class.dynamic_forms.php +++ b/include/class.dynamic_forms.php @@ -1347,11 +1347,13 @@ class DynamicFormEntryAnswer extends VerySimpleModel { } function getValue() { - if (!isset($this->_value) && isset($this->value)) { + + if (!isset($this->_value)) { //XXX: We're settting the value here to avoid infinite loop $this->_value = false; - $this->_value = $this->getField()->to_php( - $this->get('value'), $this->get('value_id')); + if (isset($this->value)) + $this->_value = $this->getField()->to_php( + $this->get('value'), $this->get('value_id')); } return $this->_value; @@ -1446,6 +1448,28 @@ class SelectionField extends FormField { return parent::getWidget($widgetClass); } + function display($value) { + global $thisstaff; + + if (!is_array($value) + || !$thisstaff // Only agents can preview for now + || !($list=$this->getList())) + return parent::display($value); + + $display = array(); + foreach ($value as $k => $v) { + if (is_numeric($k) + && ($i=$list->getItem((int) $k)) + && $i->hasProperties()) + $display[] = $i->display(); + else // Perhaps deleted entry + $display[] = $v; + } + + return implode(',', $display); + + } + function parse($value) { if (!($list=$this->getList())) diff --git a/include/class.i18n.php b/include/class.i18n.php index 3954ea0d0765a6777afc396c30e87f9fac251c05..a7e23a8ee63d3fabbcbb2e4568a25b60ab3eb3ef 100644 --- a/include/class.i18n.php +++ b/include/class.i18n.php @@ -490,6 +490,10 @@ class Internationalization { TextDomain::setDefaultDomain($domain); TextDomain::lookup()->setPath(I18N_DIR); + // Set the default locale to UTF-8. It will be changed by + // ::setLocaleForUser() later for web requests. See #2910 + TextDomain::setLocale(LC_ALL, 'en_US.UTF-8'); + // User-specific translations function _N($msgid, $plural, $n) { return TextDomain::lookup()->getTranslation() diff --git a/include/class.list.php b/include/class.list.php index 84322f7006435453f857ee5ca82dd984b4555eff..69106502be674220c0f2c36c2293932554245471 100644 --- a/include/class.list.php +++ b/include/class.list.php @@ -68,8 +68,12 @@ interface CustomListItem { function getAbbrev(); function getSortOrder(); + function getList(); + function getListId(); + function getConfiguration(); + function hasProperties(); function isEnabled(); function isDeletable(); function isEnableable(); @@ -664,10 +668,18 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem { $this->clearStatus(self::ENABLED); } + function hasProperties() { + return ($this->getForm() && $this->getForm()->getFields()); + } + function getId() { return $this->get('id'); } + function getList() { + return $this->list; + } + function getListId() { return $this->get('list_id'); } @@ -733,6 +745,10 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem { return $this->getConfigurationForm(); } + function getFields() { + return $this->getForm()->getFields(); + } + function getVar($name) { $config = $this->getConfiguration(); $name = mb_strtolower($name); @@ -768,6 +784,15 @@ class DynamicListItem extends VerySimpleModel implements CustomListItem { return $this->toString(); } + function display() { + return sprintf('<a class="preview" href="#" + data-preview="#list/%d/items/%d/preview">%s</a>', + $this->getListId(), + $this->getId(), + $this->getValue() + ); + } + function update($vars, &$errors=array()) { if (!$vars['value']) { @@ -1102,7 +1127,7 @@ implements CustomListItem, TemplateVariable { return $this->set($field, $this->get($field) | $flag); } - protected function hasProperties() { + function hasProperties() { return ($this->get('properties')); } @@ -1254,6 +1279,11 @@ implements CustomListItem, TemplateVariable { return $this->_list; } + function getListId() { + if (($list = $this->getList())) + return $list->getId(); + } + function getConfigurationForm($source=null) { if (!$this->_form) { $config = $this->getConfiguration(); @@ -1283,6 +1313,10 @@ implements CustomListItem, TemplateVariable { return $this->_form; } + function getFields() { + return $this->getConfigurationForm()->getFields(); + } + function getConfiguration() { if (!$this->_settings) { @@ -1367,6 +1401,15 @@ implements CustomListItem, TemplateVariable { return count($errors) === 0; } + function display() { + return sprintf('<a class="preview" href="#" + data-preview="#list/%d/items/%d/preview">%s</a>', + $this->getListId(), + $this->getId(), + $this->getLocalName() + ); + } + function update($vars, &$errors) { $fields = array('name', 'sort'); foreach($fields as $k) { diff --git a/include/class.plugin.php b/include/class.plugin.php index 9d15bd3595a97adac8c6f0c67d45152ea8cb159b..d565b90594e34150278e698fdfc4b30595689f05 100644 --- a/include/class.plugin.php +++ b/include/class.plugin.php @@ -550,7 +550,10 @@ abstract class Plugin { $P = new Phar($phar); $sig = $P->getSignature(); $info = array(); - if ($r = dns_get_record($sig['hash'].'.'.self::$verify_domain, DNS_TXT)) { + $ignored = null; + if ($r = dns_get_record($sig['hash'].'.'.self::$verify_domain.'.', + DNS_TXT, $ignored, $ignored, true) + ) { foreach ($r as $rec) { foreach (explode(';', $rec['txt']) as $kv) { list($k, $v) = explode('=', trim($kv)); diff --git a/include/class.thread.php b/include/class.thread.php index 31b2c5c3df702e43a2855bf736c7d93141a6b24d..b6fc9b5f15504d779dc7619d0e4a6fc384ab8bd2 100644 --- a/include/class.thread.php +++ b/include/class.thread.php @@ -197,15 +197,13 @@ class Thread extends VerySimpleModel { )); } - if ($cids) { - $this->collaborators->filter(array( - 'thread_id' => $this->getId(), - Q::not(array('id__in' => $cids)) - ))->update(array( - 'updated' => SqlFunction::NOW(), - 'isactive' => 0, - )); - } + $this->collaborators->filter(array( + 'thread_id' => $this->getId(), + Q::not(array('id__in' => $cids ?: array(0))) + ))->update(array( + 'updated' => SqlFunction::NOW(), + 'isactive' => 0, + )); unset($this->ht['active_collaborators']); $this->_collaborators = null; diff --git a/include/class.thread_actions.php b/include/class.thread_actions.php index 428793399807d245f86cf0592041606784cff557..6650ee09810b2854ce7fe01f318813d6584d6706 100644 --- a/include/class.thread_actions.php +++ b/include/class.thread_actions.php @@ -164,6 +164,11 @@ JS $old = $original; } + // Move the attachments to the new entry + $old->attachments->update(array( + 'object_id' => $entry->id + )); + // Mark the new entry as edited (but not hidden nor guarded) $entry->flags = ($old->flags & ~(ThreadEntry::FLAG_HIDDEN | ThreadEntry::FLAG_GUARDED)) | ThreadEntry::FLAG_EDITED; diff --git a/include/cli/modules/deploy.php b/include/cli/modules/deploy.php index 74a11f02c3b103a106ac495eb225f86f7acad818..eb76a165f0f9784d8b9ad1c7debf4edb735861d3 100644 --- a/include/cli/modules/deploy.php +++ b/include/cli/modules/deploy.php @@ -27,6 +27,9 @@ class Deployment extends Unpacker { 'action'=>'store_true', 'help'=>'Use `git ls-files -s` as files source. Eliminates possibility of deploying untracked files'); + $this->options['force'] = array('-f', '--force', + 'action'=>'store_true', + 'help'=>'Deploy all files, even if they have not changed'); # super(*args); call_user_func_array(array('parent', '__construct'), func_get_args()); } @@ -45,13 +48,14 @@ class Deployment extends Unpacker { * Removes files from the deployment location that no longer exist in * the local repository */ - function clean($local, $destination, $recurse=0, $exclude=false) { + function clean($local, $destination, $root, $recurse=0, $exclude=false) { $dryrun = $this->getOption('dry-run', false); $verbose = $dryrun || $this->getOption('verbose'); $destination = rtrim($destination, '/') . '/'; $contents = glob($destination.'{,.}*', GLOB_BRACE|GLOB_NOSORT); foreach ($contents as $i=>$file) { - if ($this->exclude($exclude, $file)) + $relative = str_replace($root, "", $file); + if ($this->exclude($exclude, $relative)) continue; if (is_file($file)) { $ltarget = $local . '/' . basename($file); @@ -74,12 +78,13 @@ class Deployment extends Unpacker { foreach ($folders as $dir) { if (in_array(basename($dir), array('.','..'))) continue; - elseif ($this->exclude($exclude, $dir)) + $relative = str_replace($root, "", $dir); + if ($this->exclude($exclude, "$relative/")) continue; $this->clean( $local.'/'.basename($dir), $destination.basename($dir), - $recurse - 1, $exclude); + $root, $recurse - 1, $exclude); } } if (!$contents || !glob($destination.'{,.}*', GLOB_BRACE|GLOB_NOSORT)) { @@ -123,6 +128,7 @@ class Deployment extends Unpacker { return false; $source = file_get_contents($src); + $original = crc32($source); $source = preg_replace(':<script(.*) src="([^"]+)\.js"></script>:', '<script$1 src="$2.js?'.$short.'"></script>', $source); @@ -142,11 +148,20 @@ class Deployment extends Unpacker { "$1ini_set('$2', '0'); // Set by installer", $source); - return $source; + // return FALSE if the edited contents do not differ from the + // original contents + return $original != crc32($source) ? $source : false; + } + + function isChanged($source, $hash=false) { + $local = str_replace($this->source.'/', '', $source); + $hash = $hash ?: $this->hashFile($source); + list($shash, $flag) = explode(':', $this->readManifest($local)); + return ($flag === 'rewrite') ? $flag : $shash != $hash; } - function copyFile($source, $dest, $hash=false, $mode=0644) { - $contents = $this->getEditedContents($source); + function copyFile($source, $dest, $hash=false, $mode=0644, $contents=false) { + $contents = $contents ?: $this->getEditedContents($source); if ($contents === false) // Regular file return parent::copyFile($source, $dest, $hash, $mode); @@ -154,7 +169,7 @@ class Deployment extends Unpacker { if (!file_put_contents($dest, $contents)) $this->fail($dest.": Unable to apply rewrite rules"); - $this->updateManifest($source, $hash); + $this->updateManifest($source, "$hash:rewrite"); return chmod($dest, $mode); } @@ -188,16 +203,21 @@ class Deployment extends Unpacker { $dryrun = $this->getOption('dry-run', false); $verbose = $this->getOption('verbose') || $dryrun; + $force = $this->getOption('force'); while ($line = stream_get_line($pipes[1], 255, "\x00")) { list($mode, $hash, , $path) = preg_split('/\s+/', $line); $src = $source.$local.$path; if ($this->exclude($exclude, $src)) continue; - if (!$this->isChanged($src, $hash)) + if (!$force && false === ($flag = $this->isChanged($src, $hash))) continue; $dst = $destination.$path; - if ($verbose) - $this->stdout->write($dst."\n"); + if ($verbose) { + $msg = $dst; + if (is_string($flag)) + $msg = "$msg ({$flag})"; + $this->stdout->write("$msg\n"); + } if ($dryrun) continue; if (!is_dir(dirname($dst))) @@ -241,7 +261,7 @@ class Deployment extends Unpacker { $exclusions); # Unpack the include folder $this->unpackage("$root/include/{,.}*", $include, -1, - array("*/include/ost-config.php")); + array("*/include/ost-config.php", "*.sw[a-z]")); if (!$options['dry-run']) { if ($include != "{$this->destination}/include/") $this->change_include_dir($include); @@ -249,14 +269,16 @@ class Deployment extends Unpacker { if ($options['clean']) { // Clean everything but include folder first - $this->clean($root, $this->destination, -1, - array($include, "setup/")); - $this->clean("$root/include", $include, -1, + $local_include = str_replace($this->destination, "", $include); + $this->clean($root, $this->destination, $this->destination, -1, + array($local_include, "setup/")); + $this->clean("$root/include", $include, $include, -1, array("ost-config.php","settings.php","plugins/", - "*/.htaccess")); + "*/.htaccess", ".MANIFEST")); } - $this->writeManifest($this->destination); + if (!$options['dry-run']) + $this->writeManifest($this->destination); } } diff --git a/include/cli/modules/i18n.php b/include/cli/modules/i18n.php index f4c06caec1e1c34e7cb615ccb78957256b1a97b5..2adb14a9051d758dfd678631f3fe3fa37dcaa330 100644 --- a/include/cli/modules/i18n.php +++ b/include/cli/modules/i18n.php @@ -40,6 +40,12 @@ class i18n_Compiler extends Module { 'help' => 'Add a domain to the path/context of PO strings'), 'dns' => array('-d', '--dns', 'default' => false, 'metavar' => 'zone-id', 'help' => 'Write signature to DNS (via this AWS HostedZoneId)'), + 'zlib' => array('-z', '--zlib', 'default' => false, + 'action' => 'store_true', 'help' => 'Compress PHAR with zlib'), + 'bzip2' => array('-j', '--bzip2', 'default' => false, + 'action' => 'store_true', 'help' => 'Compress PHAR with bzip2'), + 'branch' => array('-b', '--branch', 'help' => 'Use a Crowdin branch + (other than the root)'), ); var $epilog = "Note: If updating DNS, you will need to set @@ -100,7 +106,7 @@ class i18n_Compiler extends Module { $this->fail('API key is required'); if (!$options['lang']) $this->fail('Language code is required. See `list`'); - $this->_build($options['lang']); + $this->_build($options['lang'], $options); break; case 'similar': $this->find_similar($options); @@ -146,7 +152,7 @@ class i18n_Compiler extends Module { } } - function _build($lang) { + function _build($lang, $options) { list($code, $zip) = $this->_request("download/$lang.zip"); if ($code !== 200) @@ -164,19 +170,34 @@ class i18n_Compiler extends Module { @unlink(I18N_DIR."$lang.phar"); $phar = new Phar(I18N_DIR."$lang.phar"); $phar->startBuffering(); + if ($options['zlib']) + $phar->compress(Phar::GZ, 'phar'); + if ($options['bzip2']) + $phar->compress(Phar::BZ2, 'phar'); $po_file = false; + $branch = false; + if ($options['branch']) + $branch = trim($options['branch'], '/') . '/'; for ($i=0; $i<$zip->numFiles; $i++) { $info = $zip->statIndex($i); + if ($branch && strpos($info['name'], $branch) !== 0) { + // Skip files not part of the named branch + continue; + } $contents = $zip->getFromIndex($i); if (!$contents) continue; - if (strpos($info['name'], '/messages.po') !== false) { + if (fnmatch('*/messages*.po', $info['name']) !== false) { $po_file = $contents; // Don't add the PO file as-is to the PHAR file continue; } + elseif (!$branch && !file_exists(I18N_DIR . 'en_US/' . $info['name'])) { + // Skip files in (other) branches + continue; + } $phar->addFromString($info['name'], $contents); } @@ -190,9 +211,9 @@ class i18n_Compiler extends Module { } foreach ($langs as $l) { list($code, $js) = $this->_http_get( - 'http://imperavi.com/webdownload/redactor/lang/?lang=' - .strtolower($l)); - if ($code == 200 && ($js != 'File not found')) { + sprintf('https://imperavi.com/download/redactor/langs/%s/', + strtolower($l))); + if ($code == 200 && strlen($js) > 100) { $phar->addFromString('js/redactor.js', $js); break; } @@ -277,7 +298,9 @@ class i18n_Compiler extends Module { $po_header = Mail_Parse::splitHeaders($mo['']); $info = array( 'Build-Date' => date(DATE_RFC822), + 'Phrases-Version' => $po_header['X-Osticket-Major-Version'], 'Build-Version' => trim(`git describe`), + 'Build-Major-Version' => MAJOR_VERSION, 'Language' => $po_header['Language'], #'Phrases' => #'Translated' => @@ -308,6 +331,8 @@ class i18n_Compiler extends Module { if (!function_exists('openssl_get_privatekey')) $this->fail('OpenSSL extension required for signing'); + if (!$options['pkey'] || !file_exists($options['pkey'])) + $this->fail('Signing private key (-P) required'); $private = openssl_get_privatekey( file_get_contents($options['pkey'])); if (!$private) diff --git a/include/cli/modules/unpack.php b/include/cli/modules/unpack.php index 75b73353deb0f212bed2a65cb420707c5b3eaedd..75fa1090f3790f7eefed5fac82581fd075de8a60 100644 --- a/include/cli/modules/unpack.php +++ b/include/cli/modules/unpack.php @@ -99,7 +99,7 @@ class Unpacker extends Module { if (!is_file($path)) return null; - if (!preg_match_all('/^(\w+) (.+)$/mu', file_get_contents($path), + if (!preg_match_all('/^([\w:,]+) (.+)$/mu', file_get_contents($path), $lines, PREG_PATTERN_ORDER) ) { return null; @@ -156,6 +156,7 @@ class Unpacker extends Module { function unpackage($folder, $destination, $recurse=0, $exclude=false) { $dryrun = $this->getOption('dry-run', false); $verbose = $this->getOption('verbose') || $dryrun; + $force = $this->getOption('force', false); if (substr($destination, -1) !== '/') $destination .= '/'; foreach (glob($folder, GLOB_BRACE|GLOB_NOSORT) as $file) { @@ -164,10 +165,15 @@ class Unpacker extends Module { if (is_file($file)) { $target = $destination . basename($file); $hash = $this->hashFile($file); - if (is_file($target) && !$this->isChanged($file, $hash)) + if (!$force && is_file($target) + && false === ($flag = $this->isChanged($file, $hash))) continue; - if ($verbose) - $this->stdout->write($target."\n"); + if ($verbose) { + $msg = $target; + if (is_string($flag)) + $msg = "$msg ({$flag})"; + $this->stdout->write("$msg\n"); + } if ($dryrun) continue; if (!is_dir($destination)) diff --git a/include/staff/system.inc.php b/include/staff/system.inc.php index ee70faefb3dc4fd878342e65edf65bf28865d34f..20198b8f88c040d7592a9d5c512bb25c3fc5029b 100644 --- a/include/staff/system.inc.php +++ b/include/staff/system.inc.php @@ -171,18 +171,24 @@ if (!$lv) { ?> <?php foreach (Internationalization::availableLanguages() as $info) { $p = $info['path']; - if ($info['phar']) $p = 'phar://' . $p; - if (file_exists($p . '/MANIFEST.php')) { - $manifest = (include $p . '/MANIFEST.php'); ?> + if ($info['phar']) + $p = 'phar://' . $p; +?> <h3><strong><?php echo Internationalization::getLanguageDescription($info['code']); ?></strong> — <?php echo $manifest['Language']; ?> -<?php if ($info['phar']) - Plugin::showVerificationBadge($info['path']); - ?> +<?php if ($info['phar']) + Plugin::showVerificationBadge($info['path']); ?> </h3> - <div><?php echo __('Version'); ?>: <?php echo $manifest['Version']; ?>, - <?php echo __('Built'); ?>: <?php echo $manifest['Build-Date']; ?> + <div><?php echo sprintf('<code>%s</code> — %s', $info['code'], + str_replace(ROOT_DIR, '', $info['path'])); ?> +<?php if (file_exists($p . '/MANIFEST.php')) { + $manifest = (include $p . '/MANIFEST.php'); ?> + <br/> <?php echo __('Version'); ?>: <?php echo $manifest['Version']; + ?>, <?php echo sprintf(__('for version %s'), + 'v'.($manifest['Phrases-Version'] ?: '1.9')); ?> + <br/> <?php echo __('Built'); ?>: <?php echo $manifest['Build-Date']; ?> +<?php } ?> </div> -<?php } +<?php } ?> </div> diff --git a/include/staff/templates/list-item-preview.tmpl.php b/include/staff/templates/list-item-preview.tmpl.php new file mode 100644 index 0000000000000000000000000000000000000000..3ffcd1998faf69ef71af61c492f049c93d57ece3 --- /dev/null +++ b/include/staff/templates/list-item-preview.tmpl.php @@ -0,0 +1,27 @@ +<?php +$name = $item->getValue(); +if ($abbrev=$item->getAbbrev()) + $name = sprintf('%s (%s)', $name, $abbrev); + +?> +<h2><?php echo Format::htmlchars($name); ?></h2> +<hr/> + +<?php +if ($item->hasProperties()) { ?> +<div> + <table class="custom-info" width="100%"> + <?php + foreach ($item->getFields() as $f) { + if (!$f->isVisible()) continue; + ?> + <tr><td style="width:30%;"><?php echo + Format::htmlchars($f->get('label')); ?>:</td> + <td><?php echo $f->display($f->value); ?></td> + </tr> + <?php } + ?> + </table> +</div> +<?php +} ?> diff --git a/include/staff/templates/list-item-row.tmpl.php b/include/staff/templates/list-item-row.tmpl.php index 574f28e97d8cc2ca9b4e1c00c96e1b293f1aee41..b54fee23f2f4aec32524e5ed98c3e61b12eb1c3a 100644 --- a/include/staff/templates/list-item-row.tmpl.php +++ b/include/staff/templates/list-item-row.tmpl.php @@ -7,12 +7,17 @@ <input type="checkbox" value="<?php echo $id; ?>" class="mass nowarn"/> </td> <td> - <a class="field-config" + <a class="field-config preview" style="overflow:inherit" href="#list/<?php echo $list->getId(); ?>/item/<?php echo $id ?>/update" id="item-<?php echo $id; ?>" + data-preview="<?php echo + sprintf('#/list/%d/items/%d/preview', + $item->getListId(), + $item->getId()); + ?>" ><?php echo sprintf('<i class="icon-edit" %s></i> ', ($prop_fields && !$item->getConfiguration()) diff --git a/include/staff/ticket-view.inc.php b/include/staff/ticket-view.inc.php index fc37d5912a39b5bd6f8254a64772a8d9840e1f16..43097ab357e17b35f6a75b561a6a1d348739d804 100644 --- a/include/staff/ticket-view.inc.php +++ b/include/staff/ticket-view.inc.php @@ -240,7 +240,7 @@ if($ticket->isOverdue()) <table border="0" cellspacing="" cellpadding="4" width="100%"> <tr> <th width="100"><?php echo __('Status');?>:</th> - <td><?php echo ($S = $ticket->getStatus()) ? $S->getLocalName() : ''; ?></td> + <td><?php echo ($S = $ticket->getStatus()) ? $S->display() : ''; ?></td> </tr> <tr> <th><?php echo __('Priority');?>:</th> @@ -584,7 +584,8 @@ if ($errors['err'] && isset($_POST['a'])) { <label><strong><?php echo __('Collaborators'); ?>:</strong></label> </td> <td> - <input type='checkbox' value='1' name="emailcollab" id="emailcollab" + <input type='checkbox' value='1' name="emailcollab" + id="t<?php echo $ticket->getThreadId(); ?>-emailcollab" <?php echo ((!$info['emailcollab'] && !$errors) || isset($info['emailcollab']))?'checked="checked"':''; ?> style="display:<?php echo $ticket->getThread()->getNumCollaborators() ? 'inline-block': 'none'; ?>;" > diff --git a/js/filedrop.field.js b/js/filedrop.field.js index ce0bf154496ae63cdccdbe5c5537649bfde3f65d..b0bc57f15c0df300cd6a3bd1d9fce26d8d8440f5 100644 --- a/js/filedrop.field.js +++ b/js/filedrop.field.js @@ -247,7 +247,7 @@ files: [], deletable: true, shim: !window.FileReader, - queuefiles: 4 + queuefiles: 1 }; $.fn.filedropbox.messages = { diff --git a/scp/ajax.php b/scp/ajax.php index f01cf70b4bb00f0b4b4a6f17bab1c0d5d92e5a2c..de27157dc74093cf638db8a5a3e0550384e00857 100644 --- a/scp/ajax.php +++ b/scp/ajax.php @@ -71,6 +71,7 @@ $dispatcher = patterns('', url_get('^(?P<list>\w+)/items/search$', 'searchListItems'), url_get('^(?P<list>\w+)/item/(?P<id>\d+)/update$', 'getListItem'), url_post('^(?P<list>\w+)/item/(?P<id>\d+)/update$', 'saveListItem'), + url_get('^(?P<list>\w+)/items/(?P<id>\d+)/preview$', 'previewListItem'), url('^(?P<list>\w+)/item/add$', 'addListItem'), url('^(?P<list>\w+)/import$', 'importListItems'), url('^(?P<list>\w+)/manage$', 'massManageListItems'), diff --git a/scp/css/scp.css b/scp/css/scp.css index 2b12d668b35e890300911594ede78abbd0fc131c..86632ba2766ff353f499c2c13b1d4b8b60d68b16 100644 --- a/scp/css/scp.css +++ b/scp/css/scp.css @@ -2322,7 +2322,9 @@ tr.disabled th { } .label { - float: right; + display: inline-block; + position: relative; + bottom: 1px; margin-bottom: 4px; font-size: 11px; padding: 0px 7px; diff --git a/scp/js/scp.js b/scp/js/scp.js index 1e2dfa304ef14df52bd445a9bbb91e4797dc2b29..3c04b4a9cb6e17d33fd1ea22841433c1c5aaabbc 100644 --- a/scp/js/scp.js +++ b/scp/js/scp.js @@ -987,8 +987,9 @@ $(document).on('click', 'a.collaborator, a.collaborators', function(e) { e.preventDefault(); var url = 'ajax.php/'+$(this).attr('href').substr(1); $.dialog(url, 201, function (xhr) { - $('input#emailcollab').show(); - $('#recipients').text(xhr.responseText); + var resp = $.parseJSON(xhr.responseText); + $('input#t'+resp.id+'-emailcollab').show(); + $('#t'+resp.id+'-recipients').text(resp.text); $('.tip_box').remove(); }, { onshow: function() { $('#user-search').focus(); } diff --git a/setup/inc/file-unclean.inc.php b/setup/inc/file-unclean.inc.php index d84fdefd1586b13623a1169fe75c45840d5bf9dc..d9992f7f83cf0228ac37d684e29f9529083616f3 100644 --- a/setup/inc/file-unclean.inc.php +++ b/setup/inc/file-unclean.inc.php @@ -4,7 +4,7 @@ if(!defined('SETUPINC')) die('Kwaheri!'); <div id="main"> <h1 style="color:#FF7700;"><?php echo __('osTicket is already installed?');?></h1> <div id="intro"> - <p><?php echo sprintf(__('Configuration file already changed - which could mean osTicket is already installed or the config file is currupted. If you are trying to upgrade osTicket, then go to %s Admin Panel %s.'), '<a href="../scp/admin.php" >', '</a>');?></p> + <p><?php echo sprintf(__('Configuration file already changed - which could mean osTicket is already installed or the config file is corrupted. If you are trying to upgrade osTicket, then go to %s Admin Panel %s.'), '<a href="../scp/admin.php" >', '</a>');?></p> <p><?php echo __('If you believe this is in error, please try replacing the config file with a unchanged template copy and try again or get technical help.');?></p> <p><?php echo sprintf(__('Refer to the %s Installation Guide %s on the wiki for more information.'), '<a target="_blank" href="http://osticket.com/wiki/Installation">', '</a>');?></p>