Compare commits

...

564 Commits

Author SHA1 Message Date
JediKev
8d38b06196 Add Release Notes for v1.18.4 2026-06-17 22:01:48 +00:00
JediKev
93c2d125e2 Merge branch '1.17.x' into develop 2026-06-17 22:00:04 +00:00
JediKev
a1114e5298 Add Release Notes for v1.17.8 2026-06-17 21:55:42 +00:00
JediKev
13122d1ec9 Merge pull request #6959 from JediKev/security/latest-patches-06-2026
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-06-17 16:48:29 -05:00
JediKev
6558b33225 security: FAQ Print
This addresses a report submitted by Michael Bommarito in regards to
external image handling in FAQ PDF exports.
2026-06-17 16:13:23 -05:00
JediKev
2a0c388fc8 security: User AJAX
This addresses a report submitted by LXY in regards to user profile and
preview access through staff AJAX.
2026-06-17 16:13:23 -05:00
JediKev
b4ede885cc security: Thread Entry Reuse
This addresses a report submitted by LXY in regards to thread entry reuse in
new-ticket workflows.
2026-06-17 16:13:23 -05:00
JediKev
119cefe516 security: Thread Actions
This addresses a report submitted by LXY in regards to thread action
authorization for ticket and task operations.
2026-06-17 16:13:23 -05:00
JediKev
5ff9795ef3 security: User Org AJAX
This addresses a report submitted by LXY in regards to user organization
updates through staff AJAX.
2026-06-17 16:13:23 -05:00
JediKev
5600f94962 security: Pwreset Token Expiration
This addresses a report submitted by Charton93 in regards to password reset
token expiration window handling.
2026-06-17 16:13:23 -05:00
JediKev
d457c146a5 security: Template Vars
This addresses a report submitted by luke.hebenstreit@outlook.com in regards
to template variable resolution for sensitive object values.
2026-06-17 16:13:23 -05:00
JediKev
b4cc0925c2 security: Login-Required Policy
This addresses a privately reported issue in regards to enforcing
login-required policy before ticket creation.
2026-06-17 16:13:23 -05:00
JediKev
eaebe0176e security: Note AJAX
This addresses a report submitted by sliao25@m.fudan.edu.cn in regards to
Note AJAX authorization.
2026-06-17 16:13:23 -05:00
JediKev
d590a9770d security: Ticket Field View
This addresses a report submitted by Juan Felipe Oz (@JF0x0r) in regards to
ticket field view authorization.
2026-06-17 16:13:23 -05:00
JediKev
596379768a security: Hash Comparison
This addresses a report submitted by Nozomu Sasaki (Paul) in regards to
session validation hash comparison.
2026-06-17 16:13:23 -05:00
JediKev
b535782a0a security: CSRF
This addresses a report submitted by Khaled M. Alshammri (@ik0z) in regards
to CSRF enforcement for state-changing request methods.
2026-06-17 16:13:23 -05:00
JediKev
580e1c87ec security: API Key Logging
This addresses a report submitted by Khaled M. Alshammri (@ik0z) in regards
to API key handling in system logs.
2026-06-17 16:13:23 -05:00
JediKev
ba6217a718 security: Staff Password Reset
This addresses a report submitted by Khaled M. Alshammri (@ik0z) in regards
to staff password reset response handling.
2026-06-17 16:13:23 -05:00
JediKev
7bbd8ab209 security: Request Method
This addresses a report submitted by Khaled M. Alshammri (@ik0z) in regards
to method override handling for state-changing routes.
2026-06-17 16:13:23 -05:00
JediKev
fd96bbabc9 security: Recipient Name
This addresses a report submitted by Khaled M. Alshammri (@ik0z) in regards
to collaborator and recipient name rendering.
2026-06-17 16:13:23 -05:00
JediKev
e52e01005b security: File Exception Handling
This addresses a report submitted by Khaled M. Alshammri (@ik0z) in regards
to file error handling and diagnostic output.
2026-06-17 16:13:23 -05:00
JediKev
98abb059b1 security: File URL Signatures
This addresses a report submitted by Khaled M. Alshammri (@ik0z) in regards
to file URL signature validation.
2026-06-17 16:13:23 -05:00
JediKev
078516ed03 security: Inline SVGs
This addresses a report submitted by Khaled M. Alshammri (@ik0z) in regards
to inline handling of SVG attachments.
2026-06-17 16:13:23 -05:00
JediKev
6eb6b98996 security: HTTP_REFERER Typo
This addresses a report submitted by Anindya Sankar Roy (fr3ak) in regards
to staff login referrer handling.
2026-06-17 16:13:23 -05:00
JediKev
feccb6a3a9 security: API Keys
This addresses a report submitted by Anindya Sankar Roy (fr3ak) in regards
to API key generation.
2026-06-17 16:13:23 -05:00
JediKev
1e39bf1cf7 security: Thread Entry Titles
This addresses hardening identified from a report submitted by Anindya
Sankar Roy (fr3ak) in regards to thread entry title rendering.
2026-06-17 16:13:23 -05:00
JediKev
c54a6ac79d security: Email Display Names
This addresses a report submitted by Anindya Sankar Roy (fr3ak) in regards
to rendering email display names in thread entries.
2026-06-17 16:13:23 -05:00
JediKev
5afdf5450f security: Bootstrap Tooltip
This addresses a report submitted by frgranier@deloitte.fr in regards to
tooltip behavior and data attributes in sanitized content.
2026-06-17 16:13:23 -05:00
JediKev
52c366f57b security: Full-Text Search
This addresses a report submitted by Huseyn (meKhatai) Gadashov in regards
to full-text search mode handling for user-crafted search terms.
2026-06-17 16:13:23 -05:00
JediKev
2570d69892 Add Release Notes for v1.18.3 2026-01-15 17:17:15 +00:00
JediKev
51f9249b8a Merge branch '1.17.x' into develop 2026-01-15 17:14:32 +00:00
JediKev
a18df93cdb Add Release Notes for v1.17.7 2026-01-15 17:12:24 +00:00
JediKev
c3819e4d21 Revert "issue: Guest Session TTL"
This reverts commit 416b548bd8.
2026-01-15 17:04:39 +00:00
JediKev
c59b067dbf Merge pull request #6911 from JediKev/security/latest-patches-01-2026
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 11:01:28 -06:00
JediKev
d51301dbcb Merge pull request #6910 from JediKev/htmlawed/upgrade-to-1.2.15
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 10:56:59 -06:00
JediKev
d832f24332 security: Pwreset Hardening
This mitigates a report submitted by Ruben Ferreira of CSA Cyber in
regards to password reset.
2026-01-15 10:44:00 -06:00
JediKev
0e599be671 Merge pull request #6909 from JediKev/issue/regex-or-operator-2
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:55:00 -06:00
JediKev
0ce88a7f0e Merge pull request #6908 from JediKev/rebase/pull-6034
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:54:39 -06:00
JediKev
6caaa640a2 Merge pull request #6878 from nicoletta-maia/1.17.x-banlist
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:54:18 -06:00
JediKev
914474f8cd Merge pull request #6906 from JediKev/rebase/pull-6861
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:54:00 -06:00
JediKev
a923946c75 Merge pull request #6904 from JediKev/rebase/pull-6872
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:53:41 -06:00
JediKev
ef6ed9a4d4 Merge pull request #6903 from JediKev/rebase/pull-6883
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:52:40 -06:00
JediKev
dbebe9d043 Merge pull request #6902 from JediKev/issue/empty-dept-error-on-topic-update
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:52:23 -06:00
JediKev
84b8baee1e Merge pull request #6901 from JediKev/issue/pgp-smime-mail-parsing
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:52:10 -06:00
JediKev
0d6cb83bab Merge pull request #6900 from JediKev/issue/microsoft-utf8-bom
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:51:55 -06:00
JediKev
fbe4bb99ef Merge pull request #6899 from JediKev/issue/trim-user-name
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:51:30 -06:00
JediKev
a0a7a3f3a8 Merge pull request #6898 from JediKev/issue/date-column-interval
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:51:10 -06:00
JediKev
0008042ce9 Merge pull request #6887 from JediKev/issue/pop-oauth2-error-regex
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:50:50 -06:00
JediKev
baf1e0a226 Merge pull request #6874 from JediKev/issue/svg-mime-type
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:50:28 -06:00
JediKev
ea7ecbb6e1 Merge pull request #6897 from JediKev/mpdf/upgrade-to-v8.2.7
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:50:00 -06:00
JediKev
36dee625f1 Merge pull request #6896 from JediKev/rebase/pull-6868
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:43:21 -06:00
JediKev
b46099431e Merge pull request #6865 from JediKev/mail/smtputf8-backwards-compatibility
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:42:52 -06:00
JediKev
2f17b02376 Merge pull request #6895 from JediKev/rebase/pull-6857
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:41:46 -06:00
JediKev
444fd2d013 Merge pull request #6894 from JediKev/issue/kazakh-flag-css
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:41:23 -06:00
JediKev
b53bd4316b Merge pull request #6893 from JediKev/rebase/pull-6851
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 09:40:58 -06:00
JediKev
331ca48d8d issue: Regex OR Operator
This addresses issue 6817 where when using an OR operator in the Regular
Expression validation textbox for a Short Answer field it gets removed
without warning. This is due to a malformed Regex statement within the
`Format::strip_emoticons()` function. This simply removes the brackets
from the OR comparison as brackets are only used for range comparisons
(eg. `/[\x{2310}-\x{231F}]/u`). Additionally, this replaces the
duplicated Copyright symbol with the "masculine" version of degree to
cover both types of degree symbols. There are more but these are the
most common.
2026-01-15 09:37:11 -06:00
JediKev
1552cfa3fa security: Request Handling
This tightens request handling in regards to a report submitted by
Khodor Barakat.
2026-01-15 09:29:21 -06:00
JediKev
9ce7d54ed0 Merge pull request #6808 from JediKev/issue/content-management-errors
Reviewed-By: JediKev <kevin@enhancesoft.com>
2026-01-15 08:58:52 -06:00
Thomas Oster
81ec256263 join cdata table earlier 2026-01-15 08:45:46 -06:00
JediKev
8db4d3778e security: HTML/PDF Hardening
This adds hardening in HTML sanitization and PDF rendering in regards to
reports submitted by Naveen Sunkavally at Horizon3.ai.
2026-01-14 15:03:35 -06:00
JediKev
6852e7197e issue: Ensure User Login Name
This ensures the actor's login name is of an acceptable format before
attempting authentication. This also updates `is_userid()` to accept a
new param of `$verify` that is passed through to `is_email()`.
2026-01-14 15:03:35 -06:00
JediKev
d1b634a9e7 security: mPDF Print
This addresses a report submitted by Naveen Sunkavally at Horizon3 in
regards to stream wrappers, and regex.
2026-01-14 15:03:08 -06:00
JediKev
877adf564a htmLawed: Upgrade To 1.2.15
This upgrades htmLawed to 1.2.15 (latest).
2026-01-14 14:18:25 -06:00
CKeckNorthIO
1425fe1e4f Update class.file.php
Added support for WEBP file format
2026-01-14 10:50:58 -06:00
Jamie Burchell
ca13d88887 Fixes for Implicitly marking parameters as nullable 2026-01-13 16:20:48 -06:00
ololukaszuk
ae4344189d Fix typo in 'Add New Instance' title
Fixed simple typo line 8
2026-01-13 16:18:06 -06:00
JediKev
f1ba934bd9 issue: Empty Dept Error On Topic Update
This addresses an issue where when updating a Help Topic and having a
disabled Department set for example, the system will return a generic
error stating to address the errors below; but none are shown. This is
due to a missing error div that displays the Department errors. This
simply adds the div so Department errors are visible.
2026-01-13 16:13:28 -06:00
JediKev
5074c0321f issue: Microsoft UTF-8 BOM
This addresses an issue where MS started adding UTF-8 BOM to beginning
of some content which causes parsing issues and errors. This removes the
UTF-8 BOM before using it in laminas to avoid parsing issues and errors.
2026-01-13 16:09:49 -06:00
JediKev
0fbafda14a issue: Trim User Name
This adds `trim()` around the user's name when saving and updating to
avoid errant spaces.
2026-01-13 16:08:41 -06:00
JediKev
c646c8cb86 security: Ensure Session ID
This ensures the session ID is of an acceptable format before sending to
`create()`.
2026-01-13 15:51:11 -06:00
JediKev
9aff6b0317 issue: PGP + S/MIME Mail Parsing
This addresses an issue with PGP and S/MIME mail parsing where sometimes
the content is completely empty or malformed. This is due to an old PGP
block that was added back to the code after a Pear upgrade. This removes
the block to allow PGP and S/MIME mail to be parsed correctly.
2026-01-13 15:41:29 -06:00
JediKev
1e38901e79 issue: Date Column Interval
This addresses an issue where having multiple Interval values for a
single Date column (like DueDate) throws a fatal error. This is due to
the system expecting the interval to be a string not an array. This adds
a cast to each place we use the interval to ensure it's a string to
avoid fatal errors.
2026-01-13 14:49:34 -06:00
JediKev
39cdd2b1fb mPDF: Upgrade To v8.2.7
This upgrades the entire mPDF/mPDF package to the latest (v8.2.7) to
support latest PHP, etc.
2026-01-13 14:29:41 -06:00
Jamie Burchell
573e849631 Fixes for Implicitly marking parameters as nullable 2026-01-13 13:41:50 -06:00
ntozier
8b6a0f05fd Update class.setup.php
/download/ page indicates that 1.18.2 needs 8.2+.
This updates this value to match
2026-01-13 13:35:13 -06:00
Sebastián Santisi
924380a970 Fixing Argentinian flag
On commit 3ee111122b the Argentinian flag was wrongly replaced by the Palestinian flag.
2026-01-13 13:29:44 -06:00
JediKev
624c71716c issue: Kazakh Flag CSS
This simply adds the missing Kazakh flag coordinates.
2026-01-13 13:25:13 -06:00
JediKev
10f38bd0cf issue: POP OAuth2 Error REGEX
This addresses an issue with POP OAuth2 error parsing where errors are
blank on screen. This is due to using `$matches[2]` (which doesn't
exist) vs `$matches[1]`. This updates the code to use the correct match
so that errors are correctly shown on screen.
2025-12-16 10:12:38 -06:00
Nicoletta Maia
a7f14bf241 Fix Banlist permission check 2025-11-11 13:37:41 +01:00
JediKev
a6309e2224 issue: SVG MIME Type
This addresses 6873 where SVG Image uploads are failing even with strict
MIME checking disabled. This is due to the `exif_imagetype()` call that
checks an image MIME type. This method does not work well with SVGs as
it only checks the first few bytes. This adds a specific SVG check using
`mime_content_type()` that can handle SVG MIME type checks.
2025-10-22 09:58:16 -05:00
JediKev
05b87199ac mail: SMTPUTF8 Backwards Compatibility
This adds backwards compatibility support for legacy mail servers that
don’t support SMTPUTF8 or ones that simply refuse to support it (get
with the times old man!). When the system adds an attachment or adds an
inline image to outbound mail we add the raw UTF-8 filename to the
`filename=` parameter contained within the `Content-Disposition` header.
This can be problematic with legacy mail servers or servers that refuse
to support SMTPUTF8 as non-ASCII characters contained within the
`filename=` parameter of the `Content-Disposition` header triggers
SMTPUTF8 requirement. If the server doesn’t support it but it’s required
then it bounces the mail, typically with an error message stating such.

This updates the `Message::addInlineImage()` and
`Message::addAttachment()` methods to not only provide an ASCII-safe
filename but to also utilize a new MIME Part `Rfc2231Part` (discussed
momentarily). To generate said ASCII-safe filenames for both inline
images and any attached files this adds a new method called
`Message::asciiFallback()` that converts raw UTF-8 filenames to ASCII as
well as removes any non-header safe characters. In addition, this adds a
new MIME utility namespace `osTicket\Mime` that houses a new MIME
utility called `Rfc2231Part`. This class extends `Laminas\Mime\Part` to
essentially modify the `Content-Disposition` header and tack on the
original UTF-8 filename using the `filename*=UTF-8''` parameter but
encoded with `rawurlencode()` to make it ASCII-safe.

In summary, this adds an ASCII-safe `filename=` parameter to the
`Content-Disposition` header to prevent legacy servers from triggering
SMTPUTF8 requirement whilst utilizing `Rfc2231Part` to additionally
provide an (ASCII-safe) UTF-8 `filename*=UTF-8''` parameter for modern
mail servers (preserving the original filename).
2025-10-14 14:49:04 -05:00
JediKev
2a4fe6ad4f issue: Content Management Errors
This addresses an issue where if you update the User Account
Confirmation Email template for example and it has validation errors the
system will throw a fatal error. This is due to a missing `$langs`
variable and a missing `$cfg` global. This adds the missing variable and
global back to avoid fatal errors.
2025-02-13 13:28:25 -06:00
JediKev
53339df0f5 Add Release Notes for v1.18.2 2025-02-04 09:57:30 -06:00
JediKev
b3d9e39a5d Merge branch '1.17.x' into develop 2025-02-04 09:46:36 -06:00
JediKev
3ff35ad534 Add Release Notes for v1.17.6 2025-02-04 09:36:50 -06:00
JediKev
06c893a5b1 Merge pull request #6802 from JediKev/security/latest-vulns-01-2025
Reviewed-By: JediKev <kevin@enhancesoft.com>
2025-02-04 09:34:49 -06:00
JediKev
dfb1d31fae Merge pull request #6801 from protich/1.17.x
Reviewed-By: JediKev <kevin@enhancesoft.com>
2025-02-04 08:44:37 -06:00
Peter Rotich
ec76a2032e Revert part of 0784359 commit
This commit reverts part of 0784359876
commit. Specifically the wrong / invalid test.
2025-02-04 01:17:43 -08:00
Peter Rotich
a8792c3db9 Merge pull request #6800 from JediKev/issue/plain-text-base64
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-03 23:01:57 +03:00
Peter Rotich
3a77257f50 Merge pull request #6799 from JediKev/issue/transfer-empty-comments-var
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-03 23:01:25 +03:00
Peter Rotich
8d63aa0cc0 Merge pull request #6798 from JediKev/field/variable-name-validation
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-03 23:00:36 +03:00
Peter Rotich
4858f93c2d Merge pull request #6797 from JediKev/issue/canned-response-access
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-03 22:59:42 +03:00
Peter Rotich
8b5b831430 Merge pull request #6796 from JediKev/issue/iframe-logins
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-03 22:58:35 +03:00
JediKev
c4b85fab1f Merge pull request #6766 from JediKev/patches/latest-11-18-2024
Reviewed-By: protich <peter@osticket.com>, JediKev <kevin@enhancesoft.com>
2025-02-03 13:17:42 -06:00
JediKev
245e755462 issue: Random Print
This removes a random `print $emails` from the `emailtest.php` page.
Looks like it was there by mistake all this time.
2025-02-03 13:16:56 -06:00
JediKev
416b548bd8 issue: Guest Session TTL
This addresses an issue where Guest User sessions would quickly timeout
even though the User session was configured for say 24 hours. This was
due to a chuck of code that used `min()` to get the minimum value from
the various TTL. This removes the `min()` part so it uses the `$ttl` if
available else defaults to `$maxlife`/`SESSION_TTL`.
2025-02-03 13:16:46 -06:00
JediKev
9f27584f97 Merge pull request #6724 from JediKev/issue/mimeDecode-eml-attachments
Reviewed-By: protich <peter@osticket.com>, JediKev <kevin@enhancesoft.com>
2025-02-03 13:15:39 -06:00
JediKev
7fc3d8c2ee issue: mimeDecode .eml Attachments
This reapplies a missing patch `af1c4a6` that added `message/rfc822`
attachment support.
2025-02-03 13:14:52 -06:00
JediKev
ab6672faa0 security: Syslog AJAX
This addresses a vulnerability reported by Muhammad Umer Adeem
(@UmerAdeemCheema) where normal Agents can potentially view system logs.
This also addresses additional access vulns with AJAX Content.
2025-02-03 13:09:10 -06:00
Peter Rotich
193f5fe007 Sanitize query to prevent SQL injection via parameter markers
The previous implementation used the regex `/:(\d+)/i` to remove a
single colon preceding digits. However, this approach did not handle
cases with multiple colons (e.g., "::1"), which could potentially allow
for injection attempts.

This commit updates the regex to `/:+(\d+)/` to match one or more colons
before the digits and replaces them with just the captured number. This
change ensures that all leading colons are removed, effectively
neutralizing any parameter marker injection while preserving the
intended numeric value.
2025-02-03 13:04:14 -06:00
JediKev
3a5da66bf6 issue: iFrame Logins
This addresses an issue with iFrame logins where the site simply would
refuse to honor cross site cookies. This adds a new check to see if the
system allows iFrames. If so, it will set the `samesite` for the cookie
to `None` which allows the cookie to be sent in all contexts. If the
site does not allow iFrames it will default to `Strict` for security
reasons.
2025-02-03 12:55:50 -06:00
JediKev
6ba1de123c Merge pull request #6795 from JediKev/i18n/crowdin-api-v2
Reviewed-By: protich <peter@osticket.com>, JediKev <kevin@enhancesoft.com>
2025-02-03 09:52:49 -06:00
JediKev
f57fc3f7c4 Merge pull request #6789 from JediKev/issue/excessive-fetching-errors
Reviewed-By: protich <peter@osticket.com>, JediKev <kevin@enhancesoft.com>
2025-02-03 09:52:09 -06:00
Peter Rotich
b1da571fed Merge pull request #6788 from JediKev/php/8.4-support
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 11:48:11 +03:00
Peter Rotich
6eb9bc2388 Merge branch '1.17.x' into php/8.4-support 2025-02-01 11:47:04 +03:00
Peter Rotich
6fb48cdec3 Merge pull request #6785 from JediKev/issue/referred-tickets-incorrect-queue-counts
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 11:42:45 +03:00
Peter Rotich
0176ef97d9 Merge pull request #6773 from remram44/fix-setting-force_https
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 11:32:52 +03:00
Peter Rotich
a8008b73f2 Merge pull request #6770 from JediKev/issue/dept-field-user-import
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 11:26:51 +03:00
Peter Rotich
28af87ef03 Merge pull request #6768 from JediKev/issue/user-lookup-umlaut
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 11:25:39 +03:00
Peter Rotich
3a519bd710 Merge pull request #6767 from JediKev/update/php-deprecations-and-warnings
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 11:24:22 +03:00
Peter Rotich
68fd23aaf3 Merge pull request #6765 from JediKev/issue/client-acl-staff-file-download
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 11:15:48 +03:00
Peter Rotich
3326138a52 Merge pull request #6763 from JediKev/queue/getTotal-incorrect-counts
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 11:14:06 +03:00
Peter Rotich
1b9fb37d05 Merge pull request #6762 from JediKev/issue/latest-sql-warnings
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 11:10:32 +03:00
Peter Rotich
587a97c2b9 Merge pull request #6761 from JediKev/update/composer-symfony-process
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 11:08:25 +03:00
Peter Rotich
c466519e48 Merge pull request #6753 from protich/feature/oauth2-strict-mode
OAuth2: Show Scopes and Enforce Strict Mode
2025-02-01 11:07:33 +03:00
Peter Rotich
61b0d6af09 Merge pull request #6751 from JediKev/api/duedate-and-other-errors
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 11:06:00 +03:00
Peter Rotich
ede9c01b38 Merge pull request #6742 from JediKev/user/list-import
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 11:03:35 +03:00
Peter Rotich
81d84b0c2f Merge pull request #6741 from JediKev/issue/image-annotation
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 11:02:30 +03:00
Peter Rotich
241c1af156 Merge pull request #6733 from JediKev/issue/multiselect-list-export
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 11:00:45 +03:00
Peter Rotich
8eb2dfae26 Merge pull request #6732 from JediKev/issue/safari-response-content-disposition
issue: Safari Response Content Disposition
2025-02-01 10:51:05 +03:00
Peter Rotich
9ce6e5c90a Merge pull request #6730 from JediKev/issue/email-remote-bk-name
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 10:48:44 +03:00
Peter Rotich
4b4fc10842 Merge pull request #6729 from dkrotich/feature/vscode_workspace
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 10:47:26 +03:00
Peter Rotich
0355664f6d Merge pull request #6727 from JediKev/php/iconv-recommendation
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 10:46:32 +03:00
Peter Rotich
ff331e9f8d Merge pull request #6725 from JediKev/mail/reapply-memory-optimization
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 10:45:20 +03:00
Peter Rotich
a9bd930217 Merge pull request #6722 from JediKev/issue/basic-search-selector
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 10:35:15 +03:00
Peter Rotich
ea1bdbbc27 Merge pull request #6721 from JediKev/issue/ticket-relations
Reviewed-By: Peter Rotich <peter@osticket.com>
2025-02-01 10:32:45 +03:00
JediKev
0ec6670d19 i18n: Crowdin API v2
This makes the changes needed to fully support Crowdin's latest API v2.
The main changes are the endpoints and the response types. Previously
the base URL was structured like `/api/project/{project}/{command}`
however now it is structured like
`/api/v2/projects/{project_id}/{command}`. Also previously the response
types were XML whereas now they are all JSON. There are other small
changes however their is a new function added called `getBranchId()`
that retrieves the given Branch's ID (to be used in URLs and the build
stage).
2025-01-30 10:26:41 -06:00
JediKev
216ded32ec issue: Plain-text Base64
This addresses an issue where Base64 encoding plain-text content leads
to raw encoded data being displayed in the mail client. This removes the
Base64 encoding from plain-text emails so the content displays properly.
To note, when you look at providers like Gmail, etc. they do not Base64
encode Plain-text nor HTML content.
2025-01-27 14:28:28 -06:00
JediKev
c546a16729 issue: Excessive Fetching Errors
This addresses issue 6647 where in certain scenarios/setups the system
throws the infamous `Excessive errors processing emails for` error. As
per the information from the referenced issue the main cause is a fatal
error where `json_decode()` is receiving an array instead of the
expected string. This adds a check on `$fieldClean[0]` to see if it's a
string and if so uses the `json_decode()` otherwise it uses itself.
2025-01-17 09:19:07 -06:00
JediKev
b930a68b70 issue: Canned Response Access
This addresses issue 6772 where you can access a Canned Response with a
Department set even if you don’t have the Premade Role Permission for
the Canned Response's Department. This is due to not checking access
before displaying the Canned Response. This adds a new function called
`staffCanAccess()` to class Canned that checks to see if the Canned
Response has a Department and if so ensures the Agent has a Role in that
Department and that the Role contains the Premade Permission. This adds
a new `if()` statement that calls `staffCanAccess()` if a Canned
Response is found.
2025-01-16 14:20:40 -06:00
JediKev
fb9a39ba3d update: README PHP Version
This updates the PHP version in the Requirements section from `8.1 - 8.3
(8.3 recommended)` to `8.1 - 8.4 (8.4 recommended)`.
2025-01-16 13:27:39 -06:00
JediKev
5e5a9ff5da deprecation: fgetcsv/fputcsv Default Escape Param
This addresses the deprecation of using the default value for the
`escape` parameter for both `fputcsv()` and `fgetcsv()`. Essentially,
you can no longer use the default value and instead have to explicitly
pass it in. This adds the needed defaults for `escape` as well as
`separator` and `enclosure` where needed to avoid warnings and errors.

References:
https://www.php.net/manual/en/migration84.deprecated.php#migration84.deprecated.standard
https://www.php.net/manual/en/function.fgetcsv.php#refsect1-function.fgetcsv-parameters
2025-01-16 12:55:45 -06:00
JediKev
b4938b9905 deprecation: E_STRICT Constant
This addresses the deprecation of the `E_STRICT` constant. The upstream
error level has been removed and now the constant is being deprecated.
Since we don't support PHP 5.x any longer and this is now
deprecated/removed this fully removes the last mention of the `E_STRICT`
constant. So long and farewell my friend.

Reference:
https://www.php.net/manual/en/errorfunc.constants.php#constant.e-strict
2025-01-16 12:37:58 -06:00
JediKev
a4c0f674ee deprecated: Implicitly Nullable Parameter
This addresses the deprecation of an implicitly nullable parameter
followed by a mandatory parameter in the old suggested format. This
updates the old suggested format of `function name(?string $var=null,
$var2)` to the newly suggested format of `function name(?string $var,
$var2)`. This is to avoid warnings but to also avoid errors in the
future.

Reference:
https://www.php.net/manual/en/migration84.deprecated.php#migration84.deprecated.core.implicitly-nullable-parameter
2025-01-16 12:35:11 -06:00
JediKev
b42cad6fe8 issue: Referred Tickets Incorrect Queue Counts
This addresses an issue where having a Ticket Referred to you and one of
your Departments increases the Queue count erroneously. In the
`getTicketsVisibility()` function the system adds criteria to find
Tickets referred to the Agent and then later adds separate criteria to
find Tickets referred to the Agent’s Department(s). The problem is the
two separate criteria will make the same Ticket appear twice in the
results. To fix the issue we simply need to make the results distinct
via `ticket_id`. Since `getTicketsVisibility()` is essentially
generating an array of criteria we can’t add the distinction there.
Plus, it could be used for things other than counts. So we move on to
`applyVisibility()` where we use the criteria and add the distinction
there. To note, this function is only used in `SavedQueue::counts()` and
`SavedQueue::getTotal()` which would only affect counts; just what we
want.
2025-01-13 13:25:32 -06:00
JediKev
4f137dc2bd queue: getTotal Incorrect Counts
This addresses an issue where when using `getTotal()` to get the total
counts for a Queue it mistakenly includes Children Tickets. This causes
a discrepancy between the number of Tickets shown within the Queue vs
the total shown for the count. Since we ignore Children Tickets within
the Queues themselves (due to them being merged into a Parent) we should
also ignore Children within the Queue Counts. This adds a new filter to
the `$query` in `getTotal()` that filters Tickets with `ticket_pid`
equal to `NULL` or Tickets with `flags` containing `Ticket::FLAG_LINKED`
(ie. it is a Link not Merge).
2025-01-07 14:30:47 -06:00
Remi Rampin
3805bbfdf3 Fix force_https auto-enabling on settings page 2024-12-09 14:12:32 -05:00
JediKev
5c2b6a5d2e issue: Department Field User Import
This addresses an issue where if you are importing Users via CSV,
one of the Users already exists in the system, their profile has a
Department Field with a current value set, and the CSV row for their
account has a Department Field value the system will fail with an error
of `Cannot access offset of type string on string`. This is due to
blindly assuming the `$value` is an array. This adds a check to see if
the value is an array, if so it will use the `$id` otherwise it defaults
to the `$value`.
2024-11-22 10:18:04 -06:00
JediKev
504b0bfb4f issue: User Lookup Umlaut
This addresses osTicket/osTicket-plugins#293 where attempting to lookup
and select a User from an external source like LDAP with an umlaut in
their name causes an error of "User not found". This is because the URL
is encoded with `htmlentities()` which makes the umlaut character
unrecognizable. This wraps the lookup string with `html_entity_decode()`
so that the umlaut is preserved and lookup succeeds.
2024-11-20 16:02:32 -06:00
JediKev
84c14ace46 update: PHP Deprecations & Warnings
This addresses 6574 and other deprecations and warnings encountered
during testing with `error_reporting` set to `E_ALL` and both
`display_errors` and `display_startup_errors` enabled. This includes
defining optional parameters before required parameters and function
return types not matching their parent.
2024-11-20 14:35:03 -06:00
JediKev
8255b2e721 issue: Client ACL Staff File Download
This addresses 6758 where enabling the ACL for Clients and attempting to
download a file as an Agent throws an Access Denied error. This adds a
check in the `check_acl` function to see if the backend is set to Client
Portal and if the current authenticated person is an Agent. If so, it
will skip the ACL check; otherwise it will enforce the check.
2024-11-18 14:26:13 -06:00
JediKev
59f2591809 update: Composer symfony/process
This is a rewrite of 6757 that updates the `symfony/process` package to
address a recent security advisory (11/2024). Both osTicket and
laminas-mail does not use this package in production (only in dev mode)
so this only updates the `require-dev` section of the main
`composer.json` file as well as the `composer.lock` file. This is so
that if anyone uses it in dev mode they will be using the latest patched
version of `symfony/process`.

Since this package is not in-use below are the specific instructions used to
update it without installing it:
```
cd include/laminas-mail/
composer require --dev --no-update symfony/process
vim composer.lock
  -- Update symfony/process version to latest (v6.4.14)
  -- Update the `reference` for the source and distribution to commit hash
  -- Update the `time`
composer update --no-install --lock
```
2024-11-14 10:39:55 -06:00
JediKev
ed87b25759 issue: Transfer Empty Comments Var
This addresses an issue where the `comments` variable is not replaced in
the Ticket Transfer Alert if there is no comment provided. This adds a
ternary operator to the `comments` variable assignment to see if there
is a value. If so it will use the value if not it will default to an
empty string. This is so the variable can be replaced with nothing which
is the intended end result.
2024-11-07 14:16:59 -06:00
JediKev
0724d1ada2 fields: Variable Name Validation
This addresses an issue where `-` characters are allowed in Field
Variable Names when they shouldn't be. Reason being is dashes are not
allowed in MySQL Column names. Since we use the Field Variable Name as
the Column name in the respective CDATA table we cannot allow the dashes
in the Variable Names.
2024-11-07 13:55:48 -06:00
JediKev
9bdfde5a4b issue: Latest SQL Warnings
This temporarily addresses 6661 where there are a few MariaDB warnings
after `sql_mode` changes. This only includes temporary changes via code
until we enforce defaults via SQL in the next major release. This
accomplishes the following:
- Ensures a `body` is set when creating Drafts
- Ensures a `logger` is set when adding system logs
- Ensures `updated` has a date-time set when creating a new ThreadEntry
- Ensures `staff_id`|`team_id` defaults to `0` if no object ID available
- Ensures the `lock_id` is set to `0` instead of `NULL`
2024-11-07 13:43:30 -06:00
Peter Rotich
390555db36 OAuth2: Add Resource Ownership Strict Mode Check
Enforce Strict Mode when updating / saving OAuth2 credentials. At the
moment we force the check at the plugin level which is not ideal and
will be removed in the near future.

In addition the change include setting the correct error tag so we can
show the error message in context.
2024-10-28 03:43:15 -07:00
Peter Rotich
a77cf535ed Token: Show Authorized Scopes
Show authorized scopes to Token Details tab, it's very useful when
debugging authentication issues.
2024-10-28 03:43:15 -07:00
JediKev
fec70c4ae9 user: List Import
This addresses an issue where attempting to import a User with a value
for a Custom List fails to import the list item. This is due to the
parsed (JSON) value being passed to `SelectionField::parse()` which
doesn't know how to parse it. This pull updates the `if()` statement
that checks if the given value is not an array and adds
`JsonDataParser::parse($value) ?:` before the `array($value)` to see if
we can successfully parse the string. If so, it will use the parsed
array. If not, it falls back to manually converting the value into an
array.
2024-08-13 13:23:11 -05:00
JediKev
aeeb285061 issue: Image Annotation
This addresses an issue where when annotating an image and sending it,
it disappears from the final reply. This is due to REGEX that attempts
to detect inline images and determines if it should strip them or not.
This REGEX statement is greedy which means it will find as many matches
as possible. This is a problem for image annotation as one of the
classes that gets added to the image is `data-orig-annotated-image-src`.
This gets picked up by the REGEX and as a result causes the image to be
rejected since it contains a file URL instead of the expected `cid`.
This pull simply changes the first matching group to "lazy" so that it
stops on the first match. This causes the real `src` value to be picked
up and parsed which contains the expected `cid`.
2024-08-12 14:39:48 -05:00
JediKev
f3bf8553cc issue: Multiselect List Export
This addresses an issue where attempting to export a multiselection List
Field with multiple selected items only shows the first selected item.
This is due to the Item IDs containing spaces (due to the earlier
`explode()` call) so the IDs do not match the available IDs. This adds a
`trim()` around the Item IDs so that they match appropriately.
2024-07-15 09:49:13 -05:00
JediKev
14814ae28a api: DueDate and Other Errors
This addresses an issue where adding a `duedate` to API request payloads
the system simply ignores it. This is due to an _old_ bit of code (1.7 -
circa 2013) that refuses the `duedate` from any source other than
`staff`. This updates the check from `strcasecmp()` to `in_array()` so
it can check for both `staff` and `api`. This will allow a `duedate` to
manually be set from Staff created Tickets as well as API created
Tickets.

In addition, this updates the `getRequestStructure()` function to change
the single `strcasecmp()` for `email` to a `switch {}`. This is so we
can check multiple types other than `email`. This adds the previous
`email` format as a case and adds two new cases for `json` and `xml`
formats. Within these two new cases `duedate`, `slaId`, and `staffId`
are added as available fields. This is so the system doesn’t throw an
`Unexpected data received in API request` error even though these fields
are valid and accepted with JSON and XML payloads.
2024-07-11 13:36:13 -05:00
JediKev
64a5df6881 issue: Safari Response Content Disposition
This addresses an issue where Safari is now refusing to load inline
images stored in S3. Upon investigation it was found the code had an old
check for Safari browser when generating the content disposition
filename string. If Safari was being used it left the filename alone as
"Safari and only Safari can handle the filename as is". Well this may be
true for Safari however S3 cannot handle this. This removes the Safari
specific check and forces even Safari to UTF8 encode the filename for
the content disposition string. This allows Safari to load inline
images from S3 again. Safari still loads other images, etc. just fine as
well.
2024-07-08 15:02:31 -05:00
JediKev
b54dd584b6 issue: Email Remote Backend Name
This addresses an issue reported by JensEB where the wrong remote
backend name was used in a strpos check. This updates the name from
`auth_bk` to the correct `mailbox_auth_bk`.
2024-07-02 11:42:03 -05:00
Dennis Rotich
0da2e0e0f2 Ignore VS Code Workspace Files
This commits adds *.code-workspace to .gitignore file
2024-06-21 17:47:48 +03:00
JediKev
33ecc3a0b9 php: iconv Recommendation
This updates the PHP Prerequisites to include the `iconv` extension.
Iconv methods are used for email parsing (in the pear/Mail_mimeDecode
package) as well as other places internally. This adds the extension to
the installer prerequisites as well as the system information page and
the `README.md`.
2024-06-20 15:05:55 -05:00
JediKev
ab76ffe804 issue: BC Removal
This removes a function called `Mail_mimeDecode` that was added in
`cbd78ac` (originally from pull #6681) for "backwards compatibility".
Upon investigation this is not needed; so it will be removed.
2024-06-18 13:21:33 -05:00
JediKev
eaaa64d9de mail: Reapply Memory Optimization
This reapplies the Memory Optimizations added with `5ae4fe6`, `c1d0b05`,
`2c00654`, `87277dd`, and `c57c22a`. This ensures larger emails are
parsed correctly and as optimal as possible. Without optimizations, the
system typically fails at parsing such large emails.
2024-06-18 13:13:42 -05:00
JediKev
74b8bd0e04 issue: Basic Search Selector
This addresses an issue where if you load the User Directory, click the
Organization tab, go back to the User Directory tab, and attempt a user
lookup via basic-search it attempts to hit the url `undefined?q=xxx`
which is obviously not valid and fails to return results. After
investigation it was found that when clicking through the tabs vs
directly loading them causes the Javascript/jQuery to load in reverse
sequence. This means core `scp/js/scp.js` code was taking precedence
over the inline-jQuery within the Users Staff Template. In most cases
this shouldn't matter however in this particular case the input selector
in the core `scp/js/scp.js` was looking for a generic
`input.basic-search` class. We use this class for all basic searches
however the ones for user lookup and organization lookpup respectively
have their own unique ID in addition that the inline-jQuery is supposed
to select. Since the core is loading after the inline in this case that
means the core takes precedence over the inline code causing wrong URL,
etc. This simply updates the core selector to have `:not([id])` so that
it doesn't try to match an iput with class of `input.basic-search` with
an ID. Tested and working with User/Org lookups and all other
basic-searches.
2024-06-14 10:47:08 -05:00
JediKev
9426b2dd82 issue: Ticket Relations
This addresses an issue where when viewing a Parent/Child Ticket that
also has Tasks and you first click the Tasks tab, then the Related
Tickets tab, then finally click a Related Ticket the browser redirects
to `/scp/ickets.php?=xxx` with URL Not Supported error. This is due to
reusing the class name `tasks` for the related Tickets table. This
changes the `tasks` class to `relations` so that it's unique.
2024-06-14 10:20:09 -05:00
JediKev
bb4c27dfe6 Merge pull request #6709 from JediKev/patches/latest-05-28-2024
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>
2024-06-13 11:49:39 -05:00
protich
d9fe3adad2 issue: Missing From Address
This addresses an issue where sometimes mail does not have a From Address
which causes issues in `Mail_Parse::parseAddressList()`. First, this adds a
check to see if the from is set in the headers, if not we return. Next, this
adds a check to see if the From header is malformed (ie. missing a bracket),
if so it adds one. Lastly, this continues to call `parseAddressList()` as
before however this time the From should always be set.
2024-06-13 11:49:06 -05:00
protich
854cf47ce5 issue: Quoted Table Name
This addresses an issue where the table name was not properly quoted. This
sends `true` to `quote()` to ensure the table name is properly quoted.
2024-06-13 11:49:06 -05:00
JediKev
36b4c94fb7 issue: Agent Permission Info
This addresses an issue where when exporting Agents and an Agent doesn't
have a Permission or Permission lookp fails the system throws a fatal error.
This is because we try to use `array_keys()` on `null`/`false`. This
updates class Role's `getInfo()` function to return an empty array if
the `perms` are not set/null/etc.
2024-06-13 11:47:15 -05:00
JediKev
4c57bf178e Merge pull request #6697 from JediKev/php/8.3-support
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>
2024-06-13 11:45:25 -05:00
JediKev
136d372ebc deprecated: assert_options()
This removes a call to a deprecated function `assert_options()` to match
PHP 8.3 changes.
2024-06-13 11:44:42 -05:00
JediKev
387a1c04dd update: System Prerequisites (PHP)
This updates the PHP version in the System Prerequisites (used in the
installer and upgrader) from 8.0 to 8.1. Since upstream PHP dropped support
for it we will too. RIP
2024-06-13 11:44:42 -05:00
JediKev
fea5e1e043 issue: Cannot Redeclare mb_str_pad()
This addresses an issue where in PHP 8.3 our declaration of `mb_str_pad()`
conflicts with the native `mb_str_pad()`. This adds a check to see if the
function already exists or not before declaring it.
2024-06-13 11:43:54 -05:00
JediKev
b38db3725a update: README PHP Version
This updates the PHP version in the Requirements section from `8.0 - 8.2
(8.2 recommended)` to `8.1 - 8.3 (8.3 recommended)`.
2024-06-13 11:43:54 -05:00
JediKev
a3931f4667 deprecation: get_class() No Parameters
In PHP 8.3 they have deprecated the use of `get_class()` with no parameters.
Instead you should use `get_class($this)`, `self::class`, or `__CLASS__`.
This updates all calls in the code to make it 8.3 compliant. These changes
are backwards compatible with PHP 8.1-8.2.
2024-06-13 11:43:54 -05:00
JediKev
a4e088b55e Merge pull request #6694 from JediKev/mailer/sendmail-failure
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>
2024-06-13 11:40:57 -05:00
JediKev
14e2057b0e mailer: sendmail() Failure
This addresses an issue where passing a full From string (eg. `"osTicket
Alerts" <user@noemail.com>`) as the `$from` parameter for
`Mailer::sendmail()` causes a fatal laminas-mail error. This is due to the
full string being reformatted after it's already been formatted. This
adds a check for `<` and if present we will not reformat; we will
instead use the full string as-is. This also updates one of the calls
and adds a space between the display name and the actual address.
2024-06-13 11:38:18 -05:00
JediKev
73bec0f89f Merge pull request #6720 from JediKev/changyy/develop
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>
2024-06-13 11:34:45 -05:00
Yuan-Yi Chang
cbd78ac8d6 Upgrade mimeDecode.php from v1.48 to v1.5.6 2016-08-28
Update the mimeDecode.php file, which was originally version 1.48 from 2006/12/03 and has been maintained up to now. Currently, we are directly using version 1.5.6. Adjustments were made to the syntax while retaining the original getHeader() function.

This is to fix #6680 issue.
2024-06-13 11:32:32 -05:00
JediKev
af9de90d0f Merge pull request #6719 from JediKev/ProbablePrime/fix/undelivered-mail-handling
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>
2024-06-13 11:29:24 -05:00
Richard Fox
0784359876 fix(email): fix case sensitivity in "Action" header retrieval
This fixes a casing issue in the handling of the Action header while
handling bounce notices from mail servers.

Prior to this change, no emails could be flagged as a bounce email
because the Upper-Case version of Action was used. This has been
lowercases to "action".

An accompanying unit test, tests that a bounce email is properly passed
and it's attributes are setup. I have redacted a sample DSN email as
much as I thought was necessary but am happy to re-review this fixture.

As all other header access appears to be lowercase, this just appears to
be a simple typo.

This fixes:
https://forum.osticket.com/d/104433-undelivered-mail-returned-to-sender-processing
too

A huge thank you to: Hikari Akimori from the Resonite community for
their sanity checks.
2024-06-13 11:27:41 -05:00
JediKev
45e29185c8 Merge pull request #6718 from JediKev/nicoletta-maia/issue/linked-icon-annotation
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>
2024-06-13 11:19:17 -05:00
Nicoletta Maia
6c096cce4b issue: Linked Icon annotation 2024-06-13 11:18:08 -05:00
JediKev
7cc70c8bd9 Merge pull request #6717 from JediKev/changyy/hotfix/FixGetRawEmail
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>
2024-06-13 11:15:16 -05:00
Yuan-Yi Chang
1eb71197bc Added trim() around headers in the getRawEmail method 2024-06-13 11:13:18 -05:00
Yuan-Yi Chang
c26d840c5f Fix raw mail content generation
Ensure proper splitting of mail headers and mail body using "\r\n\r\n" or "\n\n" as delimiters.
2024-06-13 11:13:02 -05:00
JediKev
c7b2968b24 Merge pull request #6716 from JediKev/upgrade/laminas-mail-2.25.1
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 10:19:02 -05:00
JediKev
ef24ee9e75 Merge pull request #6707 from sergejostir/pr/fix-staff-mobile-field-class
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 10:08:31 -05:00
JediKev
4993ba7de6 Merge pull request #6706 from JediKev/mysql/update-min-version
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 10:06:53 -05:00
JediKev
498ea7f506 Merge pull request #6704 from JediKev/i18n/lang-url-param
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 10:05:04 -05:00
JediKev
5f68020b07 Merge pull request #6703 from JediKev/php/update-prereqs
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 10:03:36 -05:00
JediKev
2a6182bd36 Merge pull request #6702 from JediKev/issue/referred-closed-tickets
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 10:00:50 -05:00
JediKev
3b1334d904 Merge pull request #6700 from JediKev/issue/task-last-respondent
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 10:00:10 -05:00
JediKev
b5a3098c65 Merge pull request #6698 from JediKev/mysqli/multi-host-failure
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 09:59:01 -05:00
JediKev
15b269dd45 Merge pull request #6699 from JediKev/i18n/schedules-getDays
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 09:54:00 -05:00
JediKev
f131c54fe8 Merge pull request #6687 from JediKev/issue/position-styling-again
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 09:31:00 -05:00
JediKev
1affaceb25 Merge pull request #6684 from amiraiman/hotfix/fix-square-brackets-when-print
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 09:29:08 -05:00
JediKev
9a224d6dc0 Merge pull request #6648 from JediKev/issue/checkbox-template-variable
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 09:13:41 -05:00
JediKev
8a01025266 Merge pull request #6630 from JediKev/i18n/tasks-dept-id-missing
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 09:12:29 -05:00
JediKev
dfcd14df1b Merge pull request #6676 from sergejostir/patch-1
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 09:04:25 -05:00
JediKev
d788786b2b Merge pull request #6675 from FlapJac-k/1.17.x
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 09:02:57 -05:00
JediKev
9bef49b672 Merge pull request #6496 from nicorac/develop
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>, aydreeihn <adriane@enhancesoft.com>
2024-06-13 09:01:40 -05:00
JediKev
f658268d1f upgrade: Laminas-Mail (2.25.1)
This updates the laminas-mail package from 2.23.0 to 2.25.1 to fully support PHP 8.3.
2024-06-12 14:04:51 -05:00
JediKev
939a278cca todo: Remove Custom prepareHeaders()
This removes the custom `prepareHeaders()` method from class Sendmail as
this functionality was added upstream which is now available with the
latest update.
2024-06-12 13:56:43 -05:00
JediKev
c0a74162f0 update: Laminas-Mail
This is a rewrite of 6312 and updates the laminas-mail package from
2.10.1 to 2.23.0. This also remove unused dev dependencies.
2024-06-12 13:56:20 -05:00
JediKev
b843fb1503 issue: Sanitization Updates
This adds a couple of new sanitizations including:
- Replacing unicode spaces with normal spaces
- Stripping the Degrees and Copyright unicode symbols
2024-06-11 17:33:15 +00:00
protich
2d65cb7723 api: XML Array Key
This addresses an issue where if the `ticket` array key is missing in the
`$current` array it will throw a fatal error. This adds an `isset()` check
to ensure the array key exists and is set before using it.
2024-05-28 20:37:04 +00:00
Sergej Oštir
f8455c82ca Fix staff mobile field class 2024-05-25 22:55:19 +02:00
Sergej Oštir
6df6cd984d Fix ajax path for AddRemoteCollaborator 2024-05-25 20:10:45 +02:00
JediKev
317ab96700 mysql: Update Minimum Version
This updates the minimum required MySQL Version from 5.0 to 5.5. We've
stated "5.5 or better" for a while but never actually enforced it in our
installer/upgrader.
2024-05-22 21:20:17 +00:00
JediKev
fece6ff727 i18n: Lang URL Param
This addresses issue 6573 where the `lang` URL param is not sanity checked
and throws a fatal error if it contains anything but a string. This forces
the `lang` param to a string when passing to `strtolower()` to avoid fatal
errors. The `isLanguageInstalled()` function will still behave the same way
as before by returning `false` if passed an invalid `lang` code -or- if the
`lang` code is valid just doesn't exist in the available languages.
2024-05-22 20:24:58 +00:00
JediKev
d331a44c2d php: Update Prerequisites
This updates the PHP Prerequisites to include the `ctype` extension needed
for email fetching as part of the `laminas-mail` package. This adds the
extension to the installer prerequisites as well as the system information
page and the `README.md`. Lastly, this updates the installer prerequisites
wording and structure so they all match and look streamlined.
2024-05-21 19:32:35 +00:00
JediKev
a7b0711d65 issue: Referred Closed Tickets
This addresses an issue where Agents cannot see Referred Tickets in their
Closed Queues. This is due to the `Staff::getTicketsVisibility()` function
blindly using the `open` state instead of looking for `closed` as well. This
updates the function to include the `closed` state in the status condition.
This ensures the referred Tickets are shown in every Queue.
2024-05-20 18:43:42 +00:00
JediKev
f2facda3e6 i18n: Schedules getDays()
This addresses an issue where using a non-english language breaks the Ticket
view page for Tickets with no Due Date, SLA with grace period longer than 41
hours, and the SLA is attached to a Schedule with working days. This is due
to mistakenly translating the day names in `getDays()` function at times we
shouldn't be. PHP only likes English day names during actual date
calculations.  This adds a new method called `getTranslatedDays()` that will
return the translated day names when needed (it also caches the
translations). In addition this updates the existing `getDays()` to strictly
return the native, untranslated day names. Lastly, this updates the calls to
`getDays()` that need to be translated to `getTranslatedDays()`.
2024-05-16 14:58:07 +00:00
JediKev
5277c7ab46 issue: Task Last Respondent
This addresses an issue where `Task::getLastRespondent()` returns the wrong
Agent leading to the wrong people receiving alerts, etc. This updates the
`Task::getLastRespondent()` function to make it limit the last respondent
search to the Task's own Thread Entries rather than searching through all
entries.
2024-05-15 17:57:15 +00:00
JediKev
0a8475fbba mysqli: Multi-Host Failure
This is a rewrite of #6641. This addresses an issue where if PHP has MySQLi
Error Reporting enabled (enabled by default starting with PHP 8.1), there
are multiple configured database hosts, and one host fails to connect the
system throws a fatal error. We previously attempted to silence fatal errors
with `@` suppression however STRICT MySQLi Error Reporting bypasses this and
still throws an error. This updates the `db_connect()` and
`db_select_database()` calls in `bootstrap.php` and wraps them with a
try/catch. If either `db_connect()` or `db_select_database()` throws a fatal
exception we will catch it and set `$ferror` to the exception message. Doing
so ensures the system attempts to connect to all configured hosts without
exiting and only croaks when no hosts are reachable or the database cannot
be selected.
2024-05-15 17:35:20 +00:00
JediKev
7f03a64bd1 issue: Position Styling.. Again
This addresses an issue where the current REGEX for the "position" styling
attribute is causing issues when emails contain legit text like "position:
100 degrees". This updates the REGEX to only look for one of the possible
values instead of looking for seemingly anything.
2024-04-22 19:01:55 +00:00
Amir Aiman
2584403447 Properly set mPdf instance autoScriptToLang property to true
Based on mPdf migration guide: https://mpdf.github.io/installation-setup/installation-v6-x.html
2024-04-14 17:07:11 +08:00
Amir Aiman
872a649210 add mpdf init options to auto convert non-latin characters 2024-04-13 17:22:11 +08:00
Eslam Mohamed
cd4717a10b Fixed mailfetch error message 2024-03-12 11:37:15 +02:00
JediKev
09de958730 issue: Checkbox Template Variable
This addresses an issue where when using a Checkbox on an Email Template the
variable is replaced with `1` if checked and doesn't get replaced at all if
unchecked. This is due to the `BooleanField` class not having an `asVar()`
method so it uses the `FormField::asVar()` method which calls `to_php()`.
This method returns an integer for BooleanField which is not what we want.
This adds a new method called `asVar()` to the `BooleanField` class that
calls `toString()` which will return `Yes` if checked and `No` if unchecked.
2023-12-06 22:14:39 +00:00
JediKev
00bc6b1bf3 i18n: Tasks Department ID Missing
This addresses a small issue where the Department Name is not translated
on the Task Queues. This is due to the Task values missing the
Department ID used to generate the object_hash for the translation
table. This adds the `dept_id` to the Task Values so the translations
are properly found.
2023-10-30 09:22:46 -05:00
JediKev
4689926b2d cli: Class Not Found
This addresses a small issue where the osTicket class could not be found in
manage.php. This moves the boostrap.php include above the osTicket call to
ensure the class is loaded before use.
2023-10-25 15:45:33 +00:00
JediKev
03755768d9 Add Release Notes for v1.18.1 2023-10-25 15:34:06 +00:00
JediKev
0f08e5b0b7 Merge branch '1.17.x' into develop 2023-10-25 15:26:17 +00:00
JediKev
7c2003629d Add Release Notes for v1.17.5 2023-10-25 15:19:29 +00:00
JediKev
f878766863 Merge pull request #6627 from JediKev/format/viewable-images-bug
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2023-10-25 10:06:35 -05:00
JediKev
2a412883ae format: Viewable Images Bug
This addresses a small bug where `Format::viewableImages()` was using
unencoded quotes instead of encoded quotes to replace the `<img src=`
matches. This means when the content is added to a textarea’s
`draft-data-original` attribute the unencoded quotes break the attribute
and displays raw HTML content. This moves the `Format::htmlchars()` call
after the `preg_replace_callback()` so the quotes can be properly
encoded (if needed).
2023-10-25 10:04:01 -05:00
JediKev
4919051817 Merge pull request #6626 from JediKev/issue/user-account-creation
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2023-10-25 10:03:19 -05:00
JediKev
777e6f0b74 issue: User Account Creation
This addresses an issue with User account creation not rotating the
session properly.
2023-10-25 10:02:49 -05:00
JediKev
7c00e398a6 Merge pull request #6625 from JediKev/security/latest-vulns-09-2023
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2023-10-25 10:02:01 -05:00
JediKev
37cf8350ea security: User Ticket Creation
This addresses an issue with Topic ID param abuse on User Ticket
Creation via portal.
2023-10-25 09:59:53 -05:00
JediKev
c4ad48de5d security: Inline Image
This addresses an issue where we needed to harden inline image regex and
localize the inline images before sanitizing.
2023-10-25 09:59:53 -05:00
JediKev
88a87a3346 security: XSS List Properties
This mitigates a vulnerability reported by Vaibhav Kumar Srivastava
where XSS is possible via List Properties.
2023-10-25 09:59:53 -05:00
JediKev
04f4e61110 security: FAQ Manage Permissions
This addresses a vulnerability reported by @lujiefsi where Agents can
potentially bypass the FAQ restrictions.
2023-10-25 09:59:53 -05:00
JediKev
45d70301f6 Merge pull request #6624 from JediKev/issue/csp-headers
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2023-10-25 09:58:53 -05:00
JediKev
6228f640ac issue: CSP Headers
This adds a few more CSP Headers (`script-src`, `object-src`) to better
secure the helpdesk.
2023-10-25 09:57:58 -05:00
JediKev
d4e1c22d4f Merge pull request #6623 from JediKev/issue/canned-resp-inline-images
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2023-10-25 09:57:02 -05:00
JediKev
a503c1600e issue: Canned Response Inline Images
This addresses an issue where saving edits made to a canned response with two
or more inline images either breaks the page or causes 500 error. This is due
to incorrectly formatted array of IDs for the inline images causing further
issues with `$att->file->getId()` on render.

This adds a check in `keepOnlyFileIds()` to see if the `$value` from the array
of IDs is an array itself. If so, we will break apart the `$value` array into
`$id` (File ID) => `$value` (File Name) to match the expected format. If the
`$value` is not an array, it will use the existing format (as before). This is
important as we have two different array formats for the File IDs depending on
what method is called to generate them. We need to be able to account for both
formats. In addition, this adds the file name to the `$attachment` array in
`getAttachmentIds()` so we can save the new attachment name (if different).

Lastly, this fixes an issue where the Canned Response attachment upload field
was blindly including all attachments rather than excluding the inline images.
2023-10-25 09:55:48 -05:00
Peter Rotich
f489dde1b4 Merge pull request #6622 from protich/issue/lint
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-10-25 09:03:33 -05:00
Peter Rotich
895c7236b5 stubs: Add stubs to pass lint test 2023-10-24 19:12:56 -07:00
Peter Rotich
9be940a835 Merge pull request #6548 from fjf2002/patch-1
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-10-24 21:02:28 -05:00
Peter Rotich
6dc77c8cdb Merge pull request #6549 from fjf2002/patch-2
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-10-24 20:59:57 -05:00
Peter Rotich
9d0ba875a3 Merge pull request #6578 from JediKev/i18n/flags-position-and-codes
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-10-24 20:45:58 -05:00
Peter Rotich
842bc7e873 Merge pull request #6577 from JediKev/i18n/update-signing
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-10-24 20:45:16 -05:00
Peter Rotich
e1340dc70c Merge pull request #6566 from JediKev/issue/putenv-disabled
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-10-24 20:44:17 -05:00
Peter Rotich
177ece7c2d Merge pull request #6591 from protich/issue/php-cli
CLI: Make sure manage.php util can be executed via php-cgi CLI
2023-10-24 20:42:18 -05:00
Peter Rotich
ee7e6e5b84 Merge pull request #6609 from JediKev/mailer/undefined-smtpaccount-getname
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-10-24 20:41:48 -05:00
Peter Rotich
a37de02a16 Merge pull request #6612 from JediKev/faq/category-notes-images
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-10-24 20:40:50 -05:00
Peter Rotich
1a3326377e Merge pull request #6614 from JediKev/patches/latest-10-03-2023
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-10-24 20:40:09 -05:00
Peter Rotich
2b5fa48590 Merge pull request #6610 from JediKev/issue/systemsessionhandler
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-10-24 10:46:17 -05:00
Peter Rotich
3fd1c04fe1 Merge pull request #6611 from JediKev/issue/status-list-overflow
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-10-24 10:44:27 -05:00
Peter Rotich
c7657ae536 Merge pull request #6620 from JediKev/issue/choice-selection-field-searches
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-10-24 10:43:23 -05:00
Peter Rotich
736f3e8772 Merge pull request #6618 from JediKev/mail/novalidatecert
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-10-24 10:27:29 -05:00
Peter Rotich
89edac8cc8 Merge pull request #6616 from fillorkill/develop
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-10-24 09:02:48 -05:00
JediKev
00e22faddc issue: Choice/Selection Field Searches
This addresses a small issue where when searching for a Choice/Selection
Field value using `includes` criteria it sometimes shows records it's
not supposed to. This is due to the REGEX used to find the value. Let's
say you have two different List Options:
1. ID = 2  | Value = List Item 2
2. ID = 22 | Value = List Item 22

Now, let’s say you make a search criteria of `List => Includes => List
Item 2`. The REGEX we use is `\"?2(\"|,|$)` which will essentially match
`*2` instead of just `2`. This means it will include Tickets with with
values of `List Item 22` as it contains a `2` as well.

This updates the REGEX to add `(?<![0-9])` before the ID/Key to ensure
it does not match numbers that contain this ID/Key. Instead it will have
to be an exact match of the ID/Key.
2023-10-20 10:42:12 -05:00
JediKev
d3c140ce35 mail: NoValidateCert
This adds code to disable certificate validation for IMAP/POP3 and SMTP.
2023-10-16 09:39:01 -05:00
Jing Zhang
d4aeada1c5 Update raphael-min.js to 2.3.0
2.1.0 invokes unsafe-eval error and dashboard graph does not display if CSP turned on. This fixes the issue.
2023-10-11 11:59:37 +08:00
JediKev
1775ce7582 schedules: No Holidays
This fixes an issue where sometimes the holidays for schedules can be
empty. This adds checks to see if holidays is empty/not set in the POST
and if so we try to get the Schedule's existing Holidays and if they
don't exist we default to an empty array. This also adds a further check
to see if `$holidays` is set if not then we default to an empty array.
2023-10-04 16:37:51 -05:00
JediKev
070eefcba2 org: Domain Input Length
This is a small patch that increases the Auto Add Members From input max
length from 60 to 256 to allow more domains.
2023-10-04 16:36:16 -05:00
JediKev
64b8c81d09 mysqli: Retry Codes
This adds the concept of Retry Codes so we can attempt to reconnect to
MySQL in the event of certain error codes (2006 and 1213).
2006 - MySQL server has gone away
1213 - Deadlock found when trying to get lock; try restarting
transaction
2023-10-04 16:31:41 -05:00
JediKev
bd034712a5 logs: Email Loop Info
This adds code to append the offending mail headers when an Email Loop
is detected. This will provide more information on what email exactly is
causing the loop.
2023-10-04 16:30:13 -05:00
JediKev
4475e03ebd search: Not Includes No Value
This addresses an issue where sometimes an Agent will add Search
criteria with `!includes` but don't add a value. This adds a check to
make sure there is a value before attempting the search. If there is no
value we simply return without executing the search to avoid errors.
2023-10-04 16:28:22 -05:00
JediKev
a8d42a9de2 api: Data Array
This addresses an issue where sometimes the returned `$data` from the
parser can be a type other than an array. We expect the data to be an
array so to avoid issues/errors this adds a check to ensure `$data` is
an array before we use it.
2023-10-04 16:25:33 -05:00
JediKev
48e8501dd0 api: Check Attachments
This addresses an issue where sometimes attachments can sometimes not be
set in the `$data` array. This adds a check to ensure attachments key
isset in the `$data` array before attempting to use it.
2023-10-04 16:23:32 -05:00
JediKev
65e12297fa search: Store Saved Search Count
This adds code to store a Saved Search count on creation.
2023-10-04 16:21:29 -05:00
JediKev
ccca0f5954 faq: Category Notes Images
This addresses an issue where inline images were being broken in FAQ
Category Notes. This ensures we call `Format::viewableImages()` on both
the Category Description and Notes to ensure they are sanitized and
properly formatted.
2023-09-29 15:17:02 -05:00
JediKev
1deee342a5 issue: Status List Overflow
This addresses an issue where having more than 20 ticket statuses run
off the page when selecting ticket statuses from Changes Status action
button. This adds a check for how many status you have an if more than
20 we add styling so the list is scrollable.
2023-09-29 14:57:17 -05:00
JediKev
7e17daa68b issue: SystemSessionHandler
This addresses an issue where if using an old database such as v1.6 ST
and attempting to upgrade you get a fatal error of
`osTicket\Session\SystemSessionHandler: must implement
osTicket\Session\AbstractSessionhandler`. This is because the
`SystemSessionHandler` class does not extend `AbstractSessionHandler`
(rightfully so). We seemingly force `AbstractSessionHandler` without
taking the `SystemSessionHandler` into account. This adds a check to see
if using `SystemSessionHandler` and if so sets `$impl` as such; allowing
the check to pass and the session to be initiated. This also updates the
capitalization of the `AbstractSessionhandler` class in two places.
2023-09-29 14:48:12 -05:00
JediKev
560a4f1d63 mailer: Undefined SmtpAccount::getName()
This addresses an issue where when attempting to send mail from an
SmtpAccount we attempt to get the From name with `getName()` which
throws a fatal error. This updates class EmailAccount and adds a new
`getName()` method that gets the From name from the associated email
relation. This provides the needed name and prevents fatal error from
being thrown. In addition, this adds a further fail-safe where if no
name is returned we will use the email address as the name.
2023-09-29 14:07:42 -05:00
Peter Rotich
0caf58648f CLI: Make sure manage util can be executed via CLI
This commit changes manage.php util to use unified cli detection
routine. This ensures that cgi based CLI binaries execute the util.
2023-07-31 13:42:17 -07:00
JediKev
fcdeb542ab Merge pull request #6579 from JediKev/update/php-requirements-1.18.x
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-07-18 13:34:57 -05:00
JediKev
1c0c670b11 update: PHP Requirements 1.18.x
This updates the PHP requirements to match 1.18.x requirements.
2023-07-18 13:30:29 -05:00
JediKev
3079040e6b Merge branch '1.17.x' into develop 2023-07-18 13:25:25 -05:00
JediKev
3dd2ed35f3 Merge pull request #6562 from JediKev/update/readme-php-and-laminas
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-07-18 13:24:31 -05:00
Tinnitus97
30a4a6e900 update: External Project Links
This updates the links to all external projects used in osTicket.
2023-07-18 13:00:34 -05:00
JediKev
45f5b5b4c5 i18n: Flags Codes
This updates the `include/class.i18n.php` file to fix issues with lang
codes. When building packs for languages with codes that have locales
(eg. `sv-SE`) the pack name use underscores, not dashes. This updates
the places we lookup by lang code and replaces `-` with `_` so they are
found correctly.
2023-07-13 15:44:38 -05:00
JediKev
3ee111122b i18n: Flags Position
This updates the `css/flags.css` to correct the positioning of some
flags. This also adds `fil` code to the `include/i18n/langs.php` file to
account for Filipino language.
2023-07-13 15:42:48 -05:00
JediKev
776c0f2791 i18n: Build MANIFEST.php
This updates the code to fix issues with the `Phrases-Version`,
`Language`, `Last-Revision`, and `Version` not being added properly in
the `MANIFEST.php` file on build. This is due to importing the wrong
class as well as incorrect key names. This updates the include_once
statement as well as the key names.
2023-07-13 15:34:36 -05:00
JediKev
9870627423 i18n: Update Signing
This updates the i18n signing code to reflect the new aws-sdk-php v3
standards. This also rearranges the code to look a little nicer.
2023-07-10 15:19:07 -05:00
JediKev
88294e2d87 update: Upgrade Section & Extensions
This is a partial rewrite of 6365. This moves the Upgrade section to the
`UPGRADING.txt` file and updates the verbiage to link to the Upgrade
Guide in the docs as well as the `UPGRADING.txt` file. In addition this
updates the recommended extensions for PHP to reflect what's shown in
Admin Panel > Dashboard > Information. Lastly, this updates various
links in the `README.md` and `UPGRADING.txt` files.
2023-06-13 16:32:37 -05:00
JediKev
555c4a74b6 update: Crowdin Localization Badge
It's time we updated from the static localization badege to a more
real-time badge.
2023-06-13 16:02:57 -05:00
JediKev
3c0d5f477a issue: putenv() Disabled
This addresses issue 6564 where `putenv()` may be disabled on some
hosts. This adds a check to make sure the function exists before calling
it to avoid fatal errors.
2023-06-13 15:46:32 -05:00
JediKev
4baf9dc859 update: README.md PHP & Laminas
This updates the `README.md` file to include the newly supported PHP 8.2
as well as an update to add more verbiage to supported MySQL versions.
This also adds credit to `laminas-mail` in the OS projects listings.
2023-06-12 14:52:11 -05:00
JediKev
8f6c2ec794 Merge branch '1.18.x' into develop 2023-06-12 18:15:07 +00:00
JediKev
724de4563e Add Release Notes for v1.18 2023-06-12 18:11:05 +00:00
JediKev
3eac42e55f update: MAJOR_VERSION
This updates MAJOR_VERSION from 1.17 to 1.18.
2023-06-12 18:08:57 +00:00
JediKev
104b4bfc70 Add Release Notes for v1.18 2023-06-12 17:58:18 +00:00
JediKev
ee39fd796e Merge branch 'develop' into 1.18.x 2023-06-12 17:45:36 +00:00
JediKev
11b298680c Merge branch '1.17.x' into develop 2023-06-12 17:45:10 +00:00
JediKev
ea462cb1e6 Add Release Notes for v1.17.4 2023-06-12 17:40:33 +00:00
JediKev
39f1a4f480 Merge branch '1.17.x-develop' into 1.17.x 2023-06-12 17:22:55 +00:00
JediKev
7d60207bd0 Merge branch '1.17.x-1.16.x' into 1.17.x 2023-06-12 17:22:45 +00:00
JediKev
a22ff3b766 issue: iFrame Domain Bypass
This mitigates a vulnerability repoted by @endansdto where iFrame domain
can be bypassed due to bad regex. This updates the regex to ensure
proper filtering and checks.
2023-06-12 17:22:35 +00:00
JediKev
ca30637645 issue: XSS Image Caption
This mitigates a vulnerability reported by @cupc4k3 where XSS is possible
via image caption. This removes the possibilty for captions altogether.
2023-06-12 17:22:35 +00:00
JediKev
cecda2f2c0 issue: Multiple File Upload
This mitigates a vulnerability reported by @ManuelHu where files can
bypass checks if sent in the same request behind a valid file. This also
adds a space to fix an annoyance in the JS errors.
2023-06-12 17:22:35 +00:00
JediKev
d1f8cd0a6c issue: Draft & Inline Images
This is a two-part commit that includes a fix for inline images on
display and a patch for a possible vulnerability for Drafts. This
restructures the way we call Format::viewableImages() so that we call it
when displaying after we sanitize the data to ensure the images are not
broken. Secondly, this ensures Drafts do not skip the
`Format::htmlchars()` sanitization at all. This is a rewrite of #6511.
2023-06-12 17:22:35 +00:00
JediKev
7bbe0762d7 issue: Password Length
This addresses an issue reported by @manavparekh where allowing
excessively lengthy passwords is not a good idea. CVE-2023-30082
2023-06-12 17:22:35 +00:00
protich
f7dd4a7c24 installer: Change Email
This updates the email included in the intial installation Ticket to
`feedback@osticket.com`. This also updates the Ticket information to
include details about the documentation site.
2023-06-12 17:22:34 +00:00
JediKev
34b744b5ea issue: Email Only Attachment
This addresses an issue where emails that contain only attachment(s)
fail to process correctly. This is due to there being no `parts` for
`$part`. This adds a check for `!$part->parts` and if true (no parts) we
call `getAttachments()` on just the `$part` itself.
2023-06-12 17:22:34 +00:00
JediKev
c89454067c issue: glob() Empty Array
With PHP 8.2 `glob()` now returns an empty array if all paths are
restricted instead of `false`. This updates the only check we have on
this to check for empty array instead.
2023-06-12 17:22:34 +00:00
JediKev
544689d72b jquery: Update To 3.7.0
This updates jQuery to the latest stable release of 3.7.0.
2023-06-12 17:22:34 +00:00
JediKev
9c3ac48755 issue: strftime() Deprecation
This replaces the now deprecated `strftime()` with `date()`. This also
adds a TODO to class Format to replace an instance there. It will need
some refactoring though so we are leaving it out of this commit.
2023-06-12 17:22:34 +00:00
JediKev
8b12cfa96f issue: Relocate Typeahead JS/CSS Files
This is a rewrite of 2401 that relocates the Typeahead JS/CSS files for
people with setups that have things like HTTP Auth enabled.
2023-06-12 17:22:34 +00:00
JediKev
a0ba67febd issue: Duplicate User Copy/Paste Import
This addresses an issue where the Copy/Paste Import feature for Users
sometimes creates a duplicate account for an existing User. This occurs
when using a string like `User Name, email@domain.tld`, however, does
not occur when using a string like `User Name,email@domain.tld`. This is
due to the extra space after the comma as we are not trimming extra
spaces correctly. This adds `trim()` to the values before they are used
so that they correctly match existing values and avoids duplicates.
2023-06-12 17:22:34 +00:00
Peter Rotich
039a902252 FileUploadField: Custom File Upload Field
This commit fixes a bug where generic (ticket/task) file upload
configuration is mistakenly used on custom field file upload.
2023-06-12 17:21:30 +00:00
Peter Rotich
7fdc9949d6 File: Strict Mime Type Check
This commit adds config setting to FileUploadField to enable strict mime
type check when uploading files.
2023-06-12 17:21:30 +00:00
JediKev
b5b9e2b4a6 issue: File Upload Type
This addresses an issue where uploading a file throws an Invalid File
error even though the file is valid and there are no file restrictions.
This is due to doing `strcasecmp()` on a variable that is some times
empty. This adds a check to see if that variable has a value before
doing the comparison to avoid false positives. In addition, this adds a
new paramter called `$strict` that if true we run the mime type check.
By default `$strict` is false which skips the check. We will eventually
add a configurable setting to enable strict mime type check if preferred.
2023-06-12 17:21:30 +00:00
JediKev
2ed8adaef6 issue: i18n Audit Exports
This addresses an issue where attempting to use `All` Event type in any
other language than English the export will fail. This removes the
translation method from `All` so that the English version is used for
the option's `key`. This doesn't affect the visible word as it gets
translated downstream.
2023-06-12 17:21:30 +00:00
JediKev
8b1cd53ff1 Merge pull request #6561 from JediKev/security/latest-vulns-06-2023
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-06-12 12:14:11 -05:00
JediKev
9bb6c04464 Merge pull request #6560 from JediKev/installer/change-email
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-06-12 11:55:23 -05:00
JediKev
10e779b77d Merge pull request #6559 from JediKev/issue/email-only-attachment
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-06-12 11:55:11 -05:00
protich
a11aee294d installer: Change Email
This updates the email included in the intial installation Ticket to
`feedback@osticket.com`. This also updates the Ticket information to
include details about the documentation site.
2023-06-12 11:52:38 -05:00
JediKev
9e45f3fa95 issue: Email Only Attachment
This addresses an issue where emails that contain only attachment(s)
fail to process correctly. This is due to there being no `parts` for
`$part`. This adds a check for `!$part->parts` and if true (no parts) we
call `getAttachments()` on just the `$part` itself.
2023-06-12 11:47:48 -05:00
JediKev
0cfbc907b7 Merge pull request #6530 from empty88/patch-8
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-06-12 11:32:15 -05:00
JediKev
81cd39c84f Merge pull request #6557 from JediKev/php/8.2-support
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>, protich <peter@osticket.com>
2023-06-12 10:48:36 -05:00
JediKev
e4bfb00de1 issue: iFrame Domain Bypass
This mitigates a vulnerability repoted by @endansdto where iFrame domain
can be bypassed due to bad regex. This updates the regex to ensure
proper filtering and checks.
2023-06-12 10:09:06 -05:00
JediKev
ae37925598 issue: XSS Image Caption
This mitigates a vulnerability reported by @cupc4k3 where XSS is possible
via image caption. This removes the possibilty for captions altogether.
2023-06-12 10:08:54 -05:00
JediKev
73b997a9de issue: Multiple File Upload
This mitigates a vulnerability reported by @ManuelHu where files can
bypass checks if sent in the same request behind a valid file. This also
adds a space to fix an annoyance in the JS errors.
2023-06-12 10:08:39 -05:00
JediKev
6924417599 issue: Draft & Inline Images
This is a two-part commit that includes a fix for inline images on
display and a patch for a possible vulnerability for Drafts. This
restructures the way we call Format::viewableImages() so that we call it
when displaying after we sanitize the data to ensure the images are not
broken. Secondly, this ensures Drafts do not skip the
`Format::htmlchars()` sanitization at all. This is a rewrite of #6511.
2023-06-12 10:08:03 -05:00
JediKev
86c2ba0220 issue: Password Length
This addresses an issue reported by @manavparekh where allowing
excessively lengthy passwords is not a good idea. CVE-2023-30082
2023-06-12 10:07:27 -05:00
JediKev
c64a2611b7 issue: glob() Empty Array
With PHP 8.2 `glob()` now returns an empty array if all paths are
restricted instead of `false`. This updates the only check we have on
this to check for empty array instead.
2023-06-12 09:30:54 -05:00
Peter Rotich
848d3bcf69 Merge remote-tracking branch 'upstream/develop' into 1.18.x 2023-06-07 09:16:17 -07:00
Peter Rotich
781dbe551b Merge pull request #6540 from JediKev/jquery/update-to-3.7.0
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-06-06 11:04:32 -05:00
Peter Rotich
e5cf0109cd Merge pull request #6553 from JediKev/issue/strftime-deprecation
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-06-06 10:55:02 -05:00
Peter Rotich
aac546d0db Database: Change Plugin Name to varchar(255)
This commit modifies name field in Plugin and Plugin Instance table to
varchar(255). This is necessary since plugin can have long arse
names.
2023-06-06 08:35:53 -07:00
JediKev
3fe132c452 issue: strftime() Deprecation
This replaces the now deprecated `strftime()` with `date()`. This also
adds a TODO to class Format to replace an instance there. It will need
some refactoring though so we are leaving it out of this commit.
2023-06-05 14:10:43 -05:00
Peter Rotich
f58bcc9bd7 Merge remote-tracking branch 'upstream/develop' into develop 2023-06-05 11:48:40 -07:00
Peter Rotich
f1d28b1c6d Merge pull request #6541 from JediKev/issue/relocate-typeahead-js-css-files
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-06-05 13:18:10 -05:00
Peter Rotich
32b0ed8cd8 Merge pull request #6543 from JediKev/update/laminas-mail
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-06-05 13:16:52 -05:00
Peter Rotich
6b53ff7950 Merge remote-tracking branch 'upstream/develop' into develop 2023-06-03 07:54:56 -07:00
fjf2002
8c8a7fd120 Update upgrade.inc.php
fix typo
2023-06-02 15:28:41 +02:00
fjf2002
9fd83eba3a Update upgrade.php
fix typo
2023-06-02 15:26:48 +02:00
Peter Rotich
9fd3cf5ffd Merge pull request #6520 from JediKev/issue/dup-user-copy-paste-import
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-05-30 17:36:30 -05:00
Peter Rotich
b7b31e721d Merge pull request #6542 from JediKev/issue/remove-old-login-code
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-05-30 17:34:37 -05:00
JediKev
bb45d37a54 todo: Remove Custom prepareHeaders()
This removes the custom `prepareHeaders()` method from class Sendmail as
this functionality was added upstream which is now available with the
latest update.
2023-05-30 17:22:41 -05:00
JediKev
66fa10af51 update: Laminas-Mail
This is a rewrite of 6312 and updates the laminas-mail package from
2.10.1 to 2.23.0. This also remove unused dev dependencies.
2023-05-30 17:22:36 -05:00
JediKev
e17ad463ec issue: Remove Old Login Code
This removes old and tired code from the login pages where we throw
`Http::response(422);` to "prevent browsers from suggesting saving the
username and password". This is no longer the case as browsers no longer
play by these rules. The only way to properly do this nowadays is to set
an attribute on the form tag to disable autocomplete. We don't want to
do this either as that would prevent password managers from autofilling
the auth info.

Throwing a 422 code prevents some setups from accepting logins due to
4XX codes being error codes in nature. Removing this code seemingly has
no negative impact whatsoever.

A short backstory, this was [originally a 401
code](a99b9ce81e)
to prevent browsers from suggesting saving the auth info. Later it was
[changed to a 422
code](772d5c59a9)
to make IE play nice.
2023-05-25 13:55:48 -05:00
JediKev
62cd40648b issue: Relocate Typeahead JS/CSS Files
This is a rewrite of 2401 that relocates the Typeahead JS/CSS files for
people with setups that have things like HTTP Auth enabled.
2023-05-25 13:46:03 -05:00
JediKev
ffa23dad20 jquery: Update To 3.7.0
This updates jQuery to the latest stable release of 3.7.0.
2023-05-22 12:47:20 -05:00
JediKev
b304cdbd9c issue: Duplicate User Copy/Paste Import
This addresses an issue where the Copy/Paste Import feature for Users
sometimes creates a duplicate account for an existing User. This occurs
when using a string like `User Name, email@domain.tld`, however, does
not occur when using a string like `User Name,email@domain.tld`. This is
due to the extra space after the comma as we are not trimming extra
spaces correctly. This adds `trim()` to the values before they are used
so that they correctly match existing values and avoids duplicates.
2023-05-11 13:32:29 -05:00
JediKev
4493b1267b issue: Inline-Images Canned Responses
This addresses an issue where inline images only used in canned
responses and saved before a draft could be generated would erroneously
be deleted by the orphan file cleanup routine. This caused the inline
images to appear broken on use as they no longer exist. This is due to a
check for a `draft_id` before creating an attachment record for each
inline file. We do not need to check for a `draft_id` as we always have
a `response` in the POST with the most current data (eg. there might be
content added before a new draft can be saved). This removes the checks
in both `create` and `update` cases so that attachment records can be
created (with `inline = 1`) to avoid being deleted by file cleanup.
2023-05-11 13:27:20 -05:00
Peter Rotich
41c1edd940 Merge remote-tracking branch 'upstream/develop' into develop 2023-05-11 08:42:53 -07:00
Peter Rotich
6c8b151ae3 Merge remote-tracking branch 'upstream/1.17.x' into develop 2023-05-11 08:42:47 -07:00
eMpTy
1105cde711 Make string translatable 2023-05-02 10:36:49 +02:00
Claudio Nicora
7805cd9428 Fix PR #6283: DB column value of "local backend" is 'client' and not 'local' 2023-03-21 12:04:54 +01:00
Peter Rotich
4e570b53d2 Merge pull request #6494 from JediKev/oauth2/strict-matching-bug
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-03-16 20:09:38 -05:00
JediKev
e014ffd24c oauth2: Strict Matching Bug
This addresses an issue where we were checking strict matching when
using basic authentication. This is due to a small oversight in the smtp
save checks. This adds a line to check if auth_bk is set to oauth2
before checking strict matching.
2023-03-16 15:23:19 -05:00
Peter Rotich
ef25ee7607 Merge pull request #6493 from JediKev/issue/thread-entry-actions-z-index
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-03-16 11:52:42 -05:00
JediKev
215a0ce262 issue: Thread Entry Actions z-index
This addresses a small annoyance where the thread entry actions dropdown
always appears below the subsequent thread entry. This can be very
annoying and even hide some of the options. This adds `z-index` of
`auto` to the `thread-entry` div so that the dropdown always appears on
top.
2023-03-16 11:03:41 -05:00
JediKev
8bd22d0942 Add Release Notes for v1.17.3 2023-03-08 19:58:52 +00:00
JediKev
ca95150105 Merge branch '1.16.x' into develop 2023-03-08 19:53:07 +00:00
JediKev
30ef1cc2ae Add Release Notes for v1.16.6 2023-03-08 19:46:13 +00:00
JediKev
402992f385 Merge pull request #6485 from JediKev/issue/getRealPath-stub
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2023-03-08 13:43:46 -06:00
JediKev
32a229aa98 issue: getRealPath() Stub
This addresses an issue where lint tests fails due to `getRealPath()`
not being defined. This is from SplFileObject so we just need to add a
stub to ignore it in the tests.
2023-03-08 13:41:40 -06:00
JediKev
ead4f742ba Merge pull request #6484 from JediKev/security/latest-patches-03-2023
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-03-08 13:27:30 -06:00
JediKev
263f71a38e security: Search Params
This mitigates a vulnerability with search params.
2023-03-08 13:22:20 -06:00
Peter Rotich
731be08600 Merge remote-tracking branch 'upstream/develop' into develop 2023-03-08 09:20:02 -08:00
Peter Rotich
55b4bde204 Merge remote-tracking branch 'upstream/1.16.x' into develop 2023-03-08 09:19:49 -08:00
JediKev
ee7ce3d9c3 plugins: Path Manipulation
This mitigates a vulnerability reported by xxx where path manipulation
is possible via Plugins.
2023-03-08 10:56:30 -06:00
JediKev
e2c8f7992c xss: Email Templates
This mitigates a vulnerability reported by xxx where XSS is possible via
Email Templates.
2023-03-08 10:56:04 -06:00
JediKev
8a55c29f87 xss: Queues
This mitigates a vulnerability reported by indevi0us where XSS is
possible via Queue conditions.
2023-03-08 10:54:19 -06:00
JediKev
343a2b47e1 xss: AJAX Paths
This mitigates a vulnerability reported by indevi0us where XSS is
possible via some AJAX paths.
2023-03-08 10:53:55 -06:00
JediKev
619ce0f620 xss: Dashboard period
This mitigates a vulnerability reported by @indevi0us where XSS is
possible via the `period` parameter. This sanitizes the parameter values
before using them anywhere.
2023-03-08 10:50:47 -06:00
JediKev
9fb01bc12f xss: Roles
This mitigates a vulnerability reported by indevi0us where XSS is
possible via the `name` parameter for Roles.
2023-03-08 10:50:07 -06:00
JediKev
daee20fdd8 xss: Organization Search q
This mitigates a vulnerability reported by @indevi0us where XSS is
possible via the `q` parameter in organization lookups. This sanitizes
the parameter value before use as well as htmlchars it before adding to
JSON output.
2023-03-08 10:36:28 -06:00
JediKev
ec6043935b xss: Search parent_id
This mitigates a vulnerability reported by @indevi0us where XSS is
possible via the parent_id parameter. This forces the parameter to an
INT so that there is no chance of XSS.
2023-03-08 10:35:56 -06:00
JediKev
091ddba965 xss: System Email
This mitigates a vulnerability reported by @edr4 where XSS is possible
via System Email. This adds sanitization to the email before
saving/displaying. This also forces the other options (topic,
department, etc.) to int so there is no chance of XSS.
2023-03-08 10:35:10 -06:00
JediKev
86f9693dc6 xss: Draft Files
This mitigates an XSS vulnerability with files uploaded through drafts.
2023-03-08 10:31:12 -06:00
Peter Rotich
5290e4997a Merge pull request #6482 from protich/issue/mailer+attachments
Reviewed-By: Jedikev <kevin@enhancesoft.com>, protich <peter@osticket.com>
2023-03-08 09:51:11 -06:00
Peter Rotich
e43cc799f0 Merge pull request #6483 from JediKev/issue/mPDF-font-files
Reviewed-By: Jedikev <kevin@enhancesoft.com>, protich <peter@osticket.com>
2023-03-08 09:50:53 -06:00
Peter Rotich
8c137fb700 Merge pull request #6386 from bestlong/patch-1
Reviewed-By: Jedikev <kevin@enhancesoft.com>, protich <peter@osticket.com>
2023-03-08 09:42:12 -06:00
Peter Rotich
516b6e71ec Merge pull request #6437 from JediKev/issue/file-upload-false
Reviewed-By: Jedikev <kevin@enhancesoft.com>, protich <peter@osticket.com>
2023-03-08 09:40:50 -06:00
Peter Rotich
a09a23e355 Merge pull request #6469 from protich/JediKev/oauth2/strict-matching
Reviewed-By: Jedikev <kevin@enhancesoft.com>, protich <peter@osticket.com>
2023-03-08 09:39:50 -06:00
JediKev
8dd2ab61df issue: FileUpload False
This addresses an issue where when doing full Ticket edit with a
FileUpload field and you don't supply a file the form_entry_value for
the corresponding field updates to `false`. This causes an issue with IS
NULL criteria so this adds a check to see if a value is present, if not,
then we return `NULL` instead of JSON encoding a `false` string value.
2023-03-08 09:37:03 -06:00
JediKev
427a8aa628 issue: mPDF Font Files
This addresses an issue where some of the mPDF font files don't get
included due to the way we use the `preg_split()`. This adds the
remaining path parts to the `list()` and adds statements to update the
`$path` if such parts exist.
2023-03-08 09:20:37 -06:00
Peter Rotich
64a8abf375 lint: Fix Case Mismatch 2023-03-07 17:53:26 -08:00
Peter Rotich
8815d0874f mailer: Inline Attachments
This commit addresses an issue where inline images also gets attached as
files.
2023-03-07 17:53:26 -08:00
Peter Rotich
7efbdfd2a3 Add FileObject Interface
This commit adds File Object Interface to force uniformity when using
mixed file types like AttachmentFile and FileObject
2023-03-07 17:53:26 -08:00
Peter Rotich
ca6c1778e5 Merge remote-tracking branch 'upstream/develop' into develop 2023-03-07 12:53:02 -08:00
Peter Rotich
5c239adb57 oauth2: Support Alias/Shared Mailboxes
This commit add logic to Mail Protocol to support shared / aliased
mailboxes by setting token User.
2023-03-07 12:29:22 -08:00
JediKev
76149d65ef Merge pull request #6466 from JediKev/oauth2/strict-matching
Reviewed-By: protich <peter@osticket.com>, JediKev <kevin@enhancesoft.com>
2023-03-07 12:48:49 -06:00
JediKev
e76e91a8ee Merge pull request #6462 from JensEB/patch-2
Reviewed-By: protich <peter@osticket.com>, JediKev <kevin@enhancesoft.com>
2023-03-07 12:46:21 -06:00
JediKev
cf83b3ad38 Merge pull request #6435 from JediKev/issue/queue-counts-incorrect
Reviewed-By: protich <peter@osticket.com>, JediKev <kevin@enhancesoft.com>
2023-03-07 12:21:25 -06:00
JediKev
23719a232f Merge pull request #6427 from JediKev/issue/does-not-have-a-value
Reviewed-By: protich <peter@osticket.com>, JediKev <kevin@enhancesoft.com>
2023-03-07 12:17:51 -06:00
JediKev
ce8e0b70e4 Merge pull request #6423 from empty88/patch-2
Reviewed-By: protich <peter@osticket.com>, JediKev <kevin@enhancesoft.com>
2023-03-07 12:12:58 -06:00
JediKev
13e75be91b Merge pull request #6421 from empty88/patch-6
Reviewed-By: protich <peter@osticket.com>, JediKev <kevin@enhancesoft.com>
2023-03-07 12:10:05 -06:00
JediKev
34d978a972 Merge pull request #6395 from protich/feature/dbproxies
Reviewed-By: protich <peter@osticket.com>, JediKev <kevin@enhancesoft.com>
2023-03-07 12:04:23 -06:00
JediKev
b20a2ba861 Merge pull request #6440 from JediKev/jquery/update-to-3.6.3
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-03-07 09:48:43 -06:00
JediKev
8d39a4cc59 jquery: Update To 3.6.3
This updates jQuery to the latest stable release of v3.6.3.
2023-03-07 09:47:48 -06:00
JediKev
58c6c2246b Merge pull request #6439 from JediKev/jquery/update-jquery-ui-1.13.2
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-03-07 09:43:33 -06:00
JediKev
37f6a3f970 Merge pull request #6434 from JediKev/session/user-logged-out
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-03-07 09:41:12 -06:00
JediKev
34f913c084 Merge pull request #6428 from JediKev/issue/disabled-queues-run-counts
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-03-07 09:39:23 -06:00
JediKev
bab98e3090 Merge pull request #6426 from JediKev/print/hidden-thread-events
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-03-07 09:38:39 -06:00
JediKev
e15e1d2929 Merge pull request #6410 from JediKev/issue/featured-faqs
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-03-07 09:37:37 -06:00
JediKev
1c316688a1 Merge pull request #6409 from JediKev/issue/ticketdenied-errno
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-03-07 09:36:51 -06:00
JediKev
2e0e768f1d Merge pull request #6408 from JediKev/issue/static-forTask
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-03-07 09:35:59 -06:00
JediKev
a9f3d9145c Merge pull request #6403 from JediKev/issue/ldap-multi-instance-fatal-error
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-03-07 09:35:18 -06:00
JediKev
41af1ab738 Merge pull request #6400 from protich/issue/mailfetch+default-dept
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-03-07 09:33:27 -06:00
JediKev
ea343b7cce Merge pull request #6399 from protich/issue/misc
Reviewed-By: JediKev <kevin@enhancesoft.com>
2023-03-07 09:32:40 -06:00
Peter Rotich
1a824770e1 Merge pull request #6477 from JediKev/i18n/make-pot
Peter Rotich <peter@enhancesoft.com>
2023-03-04 13:34:22 -06:00
Peter Rotich
96794feac8 Merge pull request #6480 from JediKev/issue/dept-export
Reviewed-By: Peter Rotich <peter@osticket.com>
2023-03-04 13:32:05 -06:00
Peter Rotich
607264dea9 Merge pull request #6481 from JediKev/update/readme-php-version
Reviewed-By:  Peter Rotich <peter@osticket.com>
2023-03-04 13:31:00 -06:00
JediKev
6087bc550c update: README PHP Version
This updates the PHP version in the Requirements section from `8.0` to
`8.0 - 8.1 (8.1 recommended)` to avoid confusion with the newly released
`8.2`.
2023-03-02 14:52:32 -06:00
JediKev
5418cf2092 issue: Department Export
This addresses a small issue where when attempting to export Agents in
any Department you get a fatal error of calling non-static method
statically. This updates the export method for the Department class to
static so that exports will function and no errors are thrown.
2023-03-02 09:51:02 -06:00
JediKev
74274b5cb1 i18n: make-pot
This addresses an issue where `make-pot` command is not working
properly. This is due to PHP 8 changes that were never fully resolved.
We had to move away from the now deprecated `each()` so we switched to
`foreach()`. However, `foreach()` does not keep current position in the
iteration which we rely on for the `make-pot` command. However, you can
get around this by using `while($var = current())` and passing by
reference which keeps the current position.
2023-02-27 15:40:40 -06:00
JediKev
c0cd8b16d0 oauth2: Delete Token Banner
This updates the Delete Token functionality to show a success/error
banner when deleting the token. If successful it will delete the Token
tab and content, load the IdP tab, and show a success banner with the
JSON content. On error it will simply show an error banner with the JSON
content.
2023-02-22 16:36:37 -06:00
JediKev
f531718a56 oauth2: Strict Matching Default True
This changes the default for Strict Matching from `false` to `true` so
that new emails are strict unless explicitly set otherwise. This also
adds some descriptive comments to the getter and setter.
2023-02-16 15:41:46 -06:00
JediKev
cb3625e875 oauth2: Bug Fixes
This addresses some bugs found during testing. This adds a check to
`SmtpAccount::getConfig()` that ensures we have an email and
MailboxAccount before attempting to call `getConfig()` and adds
descriptive comments. This also removes the `isActive()` check from the
SmtpAccount `save()` method as we only care about if it's currently set to
active. Lastly, this changes the variable name for the email address
from `$email` to `$addr` so it doesn't conflict with the original
`$email` variable.
2023-02-16 15:34:54 -06:00
JediKev
ca913ba29f oauth2: Delete Token
It's about damn time.. that we add ability to delete the OAuth2 Token.
This adds a new button to the Token tab that will allow you to delete
the current Token.
2023-02-16 15:30:37 -06:00
JediKev
b7718be42d oauth2: Add Strict To Backend ID
This adds the Strict Matching value to the Backend ID for now until we
add a column for it in the database (1.18.x).
2023-02-14 15:51:11 -06:00
JediKev
7275d67bc7 oauth2: Strict Matching
This is a rewrite of osTicket/osTicket-plugins#263 where it was
attempting to implement Strict Matching. This fully implements Strict
Matching and adds functionality to check for this when retrieving a
token and when saving SMTP with Same As Remote Mailbox. This also adds a
new disabled field to the General Settings to show what email will be
used as well as a help tip that further explains Strict Matching.
2023-02-14 14:53:27 -06:00
JensEB
af83896d92 Uncaught Error: Call to a member function getId() on bool in /.../include/class.session.php:314
To prevent following error:
PHP Fatal error: Uncaught Error: Call to a member function getId() on bool in /.../include/class.session.php:314

Logged back trace:

PHP Fatal error: Uncaught Error: Call to a member function getId() on bool in /.../include/class.session.php:314
Stack trace:
#0 /.../include/class.session.php(336): osTicket\\Session\\AbstractSessionStorageBackend->getRecord()
#1 /.../include/class.session.php(281): osTicket\\Session\\AbstractSessionStorageBackend->update()
#2 [internal function]: osTicket\\Session\\AbstractSessionHandler->write()
#3 [internal function]: session_write_close()
2023-02-03 12:55:23 +01:00
JediKev
d47fc2b1cc issue: Does Not Have A Value
This addresses an issue where using the `Does Not Have A Value` criteria
on things like Short Answer Fields shows incorrect results. In fact, it
shows all Tickets with and without values for the Field. This is due to
using an overall negative filter (`column IS NULL` or `NOT column IS NOT
NULL`) on a column from a LEFT JOIN causing the backend to return NULL
for all values in that column. This adds a new variable called
`$nullable` that is set to `false` if the `$method` is `nset`. This then
passes `$nullable` to `getOrmPath()` so it can determine wether or not
to force a normal JOIN instead of LEFT JOIN so that `xxx IS NULL`/`NOT
xxx IS NOT NULL` will return the proper results. If the search method is
not `nset` then it will use LEFT JOIN as before.
2023-01-19 13:35:20 -06:00
JediKev
b896f70de6 jquery: Update jQueryUI 1.13.2
This updates jQueryUI from `1.13.1` to `1.13.2`. This also includes the
updated CSS for jQueryUI `1.13.2`.
2023-01-18 16:20:52 -06:00
JediKev
2428cc5823 issue: Queue Counts Incorrect
This addresses an issue where when a Queue is filtering by a specific
field the counts are incorrect. This is due to using only the
`constraints` instead of both `constraints` and `path_constraints`. This
updates the `counts()` method to include the `path_constraints` if set
so that the counts are properly calculated.
2023-01-11 15:41:04 -06:00
JediKev
00d409d30e session: User Logged Out
This addresses an issue where Users login, do something, and get
randomly shown the login screen. If they refresh the page it shows
them authenticated as normal again. This is due to a Session ID mismatch
in `isvalidSession()`. When `refreshSession()` calls
`regenerateSession()` it's supposed regenerate the session and update
the session ID however it updates the local ID which `refreshSession()`
later does. This updates `regenerateSession()` to where it updates the
session ID and allows `refreshSession()` to update the local ID. This
deduplicates the local ID update and corrects the ID mismatch.
2023-01-09 14:38:55 -06:00
JediKev
3ff3ce93aa issue: Disabled Queues Run Counts
This addresses an issue where Disabled Queues are still being used in
counts. This is due to grabbing any CustomQueue that has the
`FLAG_QUEUE` flag and/or having `staff_id` equal to the current Staff
member's ID. This adds another filter `Q::not(…'flags__hasbit' =>
CustomQueue::FLAG_DISABLED,` that will ensure the Queue is not Disabled
before using in counts.
2023-01-04 11:19:36 -06:00
JediKev
f63fc67d23 print: Hidden Thread Events
This addresses an issue reported on the Forum where when a field is set
to not visible to Users it is hidden from the Staff Ticket Print PDF.
This is due to the ThreadEvent MODE that is being used (MODE_CLIENT).
This uses the User Ticket Print template instead of the Staff Ticket
Print template which hides some thread events and other data. This
updates the utilized MODE to MODE_STAFF so that the appropriate data is
printed. This also addresses an issue where if the Staff has Relative
time configured it shows in the Ticket Print which should not happen.
This adds a check for `$_REQUEST['a']` and if it's set we make sure it’s
not `print` to avoid printing relative times.
2023-01-03 13:50:52 -06:00
eMpTy
46c005a02b add translation for task status 2022-12-29 11:01:06 +01:00
eMpTy
cb52804450 Add translation for "last resort" standard column labels 2022-12-28 15:29:32 +01:00
JediKev
8df095817f issue: Featured FAQs
This addresses a small logic issue where we are adding Public and
Featured FAQs in the Featured sidebar when it should only be Featured
ones. This updates the ORM statement to only show FAQs that are set to
Featured and belong to a Public or Featured Category.
2022-12-13 15:12:41 -06:00
JediKev
6de7cf4c2c issue: TicketDenied errno
This addresses an issue where emails being rejected by Banlist is
causing excessive processing errors and refuses to move the mail. This
is due to our logic expecting `403` exception code for TicketDenied but
we are using `500` in `TicketApiController::createTicket()`. This
updates the `exerr()` line to use the `$errors['errno']` if it exists
otherwise default to `500`.
2022-12-13 13:33:24 -06:00
JediKev
1079011fc0 issue: Static forTask()
This addresses an issue where printing a Ticket with Tasks or printing a
Task itself will throw a fatal error of `Non-static method
DynamicFormEntry::forTask() cannot be called statically`. This updates
the `forTask()` method to be static to avoid fatal errors.
2022-12-12 12:10:37 -06:00
JediKev
f64860441e issue: LDAP Multi-Instance Fatal Error
This addresses an issue where if you have more than one instance of LDAP
plugin enabled and try to lookup a User it fails with a fatal error of
`Object of class StaffLDAPAuthentication could not be converted to
string`. This is due to using `array_unique()` which can't convert
objects to strings. This updates the method and adds the `SORT_REGULAR`
flag so that it can compare without converting objects to strings.

In addition this addresses an issue where we were using static ID for
StaffLDAPAuthentication backend. We updated the plugin code to reflect
the actual Backend ID but the current AJAX regex for this doesn’t match
this format. This updates the AJAX regex to match the updated format.
2022-12-05 17:09:35 -06:00
Peter Rotich
f410d4ec58 fetcher: Default Department
When fetching a mailbox, osTicket routes fetched emails to a department based
on `To` or `Delivered-To` header entries. This allows for ability to fetch
aliased emails from one primary account and route them based on configured
system emails. On failure to match, the email (ticket) is routed to the
department of the email account being fetched.

With the email backend overhaul in v1.17 - a bug was instroduced where
unmatched emails ended up being routed to default department instead of the
department of the email being fetched.

This commit addresses the issue...
2022-12-04 23:22:25 +00:00
Peter Rotich
389c669728 Merge remote-tracking branch 'upstream/develop' into develop 2022-11-30 08:42:52 +00:00
Peter Rotich
10af29a5d7 email: Account for possible Orphaned SMTP Accounts 2022-11-30 08:42:52 +00:00
Peter Rotich
ebbebaab38 Merge remote-tracking branch 'upstream/1.17.x' into develop 2022-11-30 08:42:52 +00:00
Peter Rotich
eac9960a51 cron: Local Cron as CLI 2022-11-30 08:42:52 +00:00
Peter Rotich
24fd5075d6 logs: System Logs Tooltip
This commit addresses a bug introduced in d4d9c4242 due to conflicting function
names in Api Controler.
2022-11-30 08:42:52 +00:00
Peter Rotich
e6beeb9baf api: API Request Format
This commit addresses a bug introduced by d4d9c4242 by making sure format is
taken into account when processing API Requests.
2022-11-30 08:42:52 +00:00
Peter Rotich
246aaa4dcf mysql: Support Sidecar Database Proxies
When running osTicket in a multi-node cluster setup, it makes sense to connect
to the database via a load balancer or a database proxy (assuming VIP with
failover support). It's great for redundancy -  however - this adds extra time
(miliseconds) because of the extra network hop latency.

To save a few miliseconds - when osTicket (the Application) is also hosted on
the same node or container running the database proxy - then it might make
sense to attempt a local connection first before attempting via via the VIP
(sidecar pattern).

This commit adds multiple database hosts support, comma separated, when defining
DBHOST in osTicket config file (`ost-config.php`). It also adds a new _Database
Option_ to define `DBCONNECT_TIME` used to set `MYSQLI_OPT_CONNECT_TIMEOUT`.
Connection timeout is important with multiple hosts so connection attempts
(done sequencially) can timeout relatively quickly.
2022-11-24 19:09:54 +00:00
JediKev
61263635d9 Merge branch '1.17.x' into develop 2022-11-23 18:33:45 +00:00
JediKev
8d9ab037b3 Merge branch '1.16.x' into 1.17.x 2022-11-23 18:33:35 +00:00
JediKev
460c5fd4a5 Merge pull request #6381 from protich/1.17.x
Reviewed-By: JediKev <kevin@enhancesoft.com>
2022-11-23 12:32:19 -06:00
JediKev
58fd151393 Merge pull request #6379 from protich/issue/session+regenerate
Reviewed-By: JediKev <kevin@enhancesoft.com>
2022-11-23 12:31:37 -06:00
Peter Rotich
3d7b2394be Merge pull request #6385 from JediKev/session/collab-view-ticket
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-11-23 12:27:02 -06:00
Peter Rotich
0a4498b22e Lint Fixes 2022-11-23 18:22:10 +00:00
Shao Yu-Lung (Allen)
a6d4761a85 fix: typo 2022-11-23 18:55:11 +08:00
JediKev
6afe0efa37 session: Collab View Ticket
This addresses an issue where if a Collaborator views a Ticket via the
Check Ticket Status button in the Client Portal and then tries to repeat
the process with an additional Ticket it will redirect to login page.
This is due to when instantiating the session we use `$user->getId()`
which causes a problem when the User is a Collaborator. When
instantiating a session for a Collaborator object `getId()` returns the
Collaborator ID, not the Collaborator's User ID. This updates the call
from `getId()` to `getUserId()` which gets the correct User ID from the
Collaborator object (and others).
2022-11-22 15:51:01 -06:00
Peter Rotich
d4d9c4242f fetcher: Mail Fetcher / Parser Error Handling
Prior to osTicket `v1.17.x` email fetching required and used php-imap module to
decode and parse emails, whereas locally or remotely piped emails (via API
endpoint) used a Raw Email Parser ("Mail Parser").

Using two separate backends meant duplicated code, logic and inconsistent email
processing.

Starting with osTicket `v1.17.0`, we overhauled the Email Backend to OAuth2
support and in the process we switched toxusing a unified Mail Parser for both
fetched and piped emails. To determined how the email got delivered in order to
handle errors for the interfaces accodingly, we relied on `PHP_SAP`I /
`php_sapi_name()`.

That was shortsighted because of the value of `PHP_SAPI` can be different in
the following cases;

1. Email Fetching can be triggered by autocron (Staff Activity) or by a remote
task (API)

2. `PHP_SAPI` cannot be relied on for php installations using php-fpm, php-cgi
or custom binaries for php CLI executable

3. Cron Job / Task entry "user-name" or "command path" might use separate php
executables (related to `#2`)

4. Triggering cron/php via php-fpm socket connection e.g using FastCGI Client

Since emails are fetched in batches - Email Fetching, unlike stateless API
requests, should be considered a CLI operation regardless of how it gets
trigerred, this necessary so the system can gracefully handle fetch and parse
errors.

This commit does just that and a few other minor logic / code cleanup.

~ Peter - with love!
2022-11-18 11:49:12 +00:00
Peter Rotich
d5853245f7 session: Regenerate Session Id
To avoid session fixation, osTicket regenerates Session Id on-login and on
every session timeout set for the User/Agent even with activity.

This commit changes the logic to make sure that the regenerate `TIME BOMB` only
goes off on a GET request - this is necessary so we don't invalidate data
in-flight on a POST request since osTicket Forms API backend uses session_id to
generate form field names as well as CSRF token used on forms.
2022-11-15 22:38:09 +00:00
JediKev
8fbc7eea6a Add Release Notes for v1.17.2 2022-11-14 22:16:50 +00:00
JediKev
f6c8d18495 Merge branch '1.16.x' into 1.17.x 2022-11-14 22:13:08 +00:00
JediKev
b48bf7eb0b Add Release Notes for v1.16.5 2022-11-14 22:08:06 +00:00
JediKev
5ae737b9bb Merge branch 'develop' into 1.16.x 2022-11-14 22:03:32 +00:00
JediKev
9530965f8c Merge pull request #6373 from protich/issue/session+max_ttl
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-14 16:00:20 -06:00
JediKev
7f5f8cb747 Merge pull request #6371 from protich/issue/email_validator
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-14 15:59:39 -06:00
JediKev
d2ae93b540 Merge pull request #6370 from protich/issue/user-create
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-14 15:58:56 -06:00
JediKev
0541faa826 Merge pull request #6369 from protich/issue/email
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-14 15:58:25 -06:00
JediKev
c17aa40852 Merge pull request #6368 from protich/issue/mailparse+filename
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-14 15:58:00 -06:00
JediKev
396d307913 Merge pull request #6366 from protich/1.17.x
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-14 15:56:52 -06:00
JediKev
8f5f563973 Merge pull request #6363 from JediKev/issue/html-typo
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-14 15:56:32 -06:00
JediKev
9c2d65ffe4 Merge pull request #6361 from sajtiii/develop
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-14 15:56:05 -06:00
JediKev
fd4acd4298 Merge pull request #6360 from rinkp/fix/sessiondata
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-14 15:48:22 -06:00
Peter Rotich
133585f8cd mailer: Use Namespaced Mailer
This commit instantiates the mailer using the correct namespace.
2022-11-14 21:24:43 +00:00
Peter Rotich
ea3e03d212 session: Memcache Max TTL
When storing session data in Memchache storage backend we need to make sure
the TTL is not longer than 30 days, maximum allowed.
2022-11-14 15:49:45 +00:00
Peter Rotich
55fbfcb57b user: Make sure User has a valid Email on create
Emailed tickets skips user validation on purpose with the assumption that the
user has at least a valid email. The assumption can be problematic when
osTicket has issues parsing the from address due to invalid format.
2022-11-13 21:43:28 +00:00
Peter Rotich
3c4a20a85f validator: Email Domain Verification
To truly verify an email, osTicket uses DNS lookup by checking MX record, if
that fails it defaults to checking A / AAAA record of the email domain.

This commit handle a case where a domain doesn't have A record.
2022-11-13 20:51:24 +00:00
Peter Rotich
c19a5f03e9 smtp: Only init SMTP if we have credentials 2022-11-13 18:52:37 +00:00
Peter Rotich
de620fe602 email: Clear Last Error On Save 2022-11-13 18:52:37 +00:00
Peter Rotich
c806721298 mailparse: Parse filenames with complex format 2022-11-13 18:36:04 +00:00
JediKev
50dfe93133 issue: Html Typo
This addresses a small typo where the class `Html` was used instead of
`Http`.
2022-11-10 09:23:05 -06:00
sajtiii
04c0bb3a00 Fix trusted proxy wildcard check 2022-11-10 11:28:15 +01:00
rinkp
cf8b8ce721 Fix old reference to SessionData 2022-11-10 11:13:50 +01:00
JediKev
58bf538df9 Add Release Notes for v1.17.1 2022-11-09 21:16:42 +00:00
JediKev
97c50c197b Merge branch 'develop' into 1.17.x 2022-11-09 21:14:00 +00:00
JediKev
1d11ee674f Add Release Notes for v1.16.4 2022-11-09 20:51:24 +00:00
JediKev
f7ab9f42d5 Merge branch 'develop' into 1.16.x 2022-11-09 20:49:10 +00:00
JediKev
095dc0118c Merge pull request #6358 from protich/issue/session_revisited
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-09 14:42:07 -06:00
Peter Rotich
4fc5f5cc17 session: Add routines to get and filter session records
This commit adds the base functions to get session records based on desired
criteria. It also fixes errant error_log as well as a typo.
2022-11-09 05:05:12 +00:00
Peter Rotich
49d91b0551 session: On-demand Session Destroy
This commit adds ability to ask backend to destroy a session by session_id.
Also changes how expire routine works - passing zero ttl results in the session
being destroyed immediately.
2022-11-09 04:39:07 +00:00
JediKev
e1d759c4d5 Merge pull request #6357 from JediKev/security/username-xss
Reviewed-By: JediKev <kevin@enhancesoft.com>
2022-11-08 14:57:27 -06:00
JediKev
5213ff138c security: Username XSS
This mitigates a vulnerability reported by legoclones on
[huntr.dev](https://huntr.dev/) where XSS can be stored/executed via a
User's `username`. If an Agent is redirected to the User Lookup AJAX
response directly it will execute the stored XSS. This adds
`Format::sanitize()` to the `username` before saving to the database so any
potential XSS is sanitized. Additionally, this adds `Format::htmlchars()` to
the `username` before displaying in the UI to help prevent code execution.
2022-11-08 20:54:15 +00:00
JediKev
dc0507e3b8 Merge pull request #6356 from JediKev/security/refresh-url
Reviewed-By: JediKev <kevin@enhancesoft.com>, protich <peter@osticket.com>
2022-11-08 14:51:31 -06:00
protich
3702a4fb3f security: Refresh URL
This initially targeted a particular vulnerability with the Advanced
Search Refresh URLs but subsequently evolved into unifying all Refresh
URLs. We decided to create methods that properly encode URLs to prevent
code execution and to ensure they are all uniformly created. This adds
such capabilities and updates current Refresh URL generation to the new
methods. This also adds a method `Format::http_query_string()` that will
filter the query string and remove any unwanted params. Lastly, this
removes the old and tired `Misc::currentURL()` in favor of the new
"hip" (is that what the kids say these days?) method `Http::url()`.
2022-11-08 14:48:29 -06:00
JediKev
2832d44362 Merge pull request #6345 from protich/issue/fetcher+expunge
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-08 14:40:26 -06:00
JediKev
1c42e26a0e Merge pull request #6334 from protich/issue/session++
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-08 14:38:57 -06:00
JediKev
08b5eea771 Merge pull request #6349 from protich/feature/mailparse-error-handling
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-08 14:37:48 -06:00
JediKev
406b06f284 Merge branch '1.17.x' into feature/mailparse-error-handling 2022-11-08 14:36:46 -06:00
JediKev
2d8bd7498f Merge branch '1.17.x' into issue/fetcher+expunge 2022-11-08 14:35:13 -06:00
JediKev
22db3eca53 Merge pull request #6304 from protich/issue/emailaccount-isactive
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-08 14:33:55 -06:00
Peter Rotich
f85f9036a4 mailparse: Misc. Fixes 2022-11-08 19:31:57 +00:00
Peter Rotich
46b7899bc7 mailparse: Thread Entry on Parse Error
Add mail parse error as the Message instead of an empty entry.
2022-11-08 19:31:57 +00:00
Peter Rotich
678098d874 mailparse: FileName On Parse Error
Use subject as a filename if available otherwise default to Message Id
2022-11-08 19:31:57 +00:00
Peter Rotich
30ad9cd477 api: Don't validate email requests
Skip validating the structure of email request - this is important since emails
don't necessarily have all User Information - e.g name and should be failure
resistant.
2022-11-08 19:31:57 +00:00
Peter Rotich
ba2d31cd68 api: Pass Ticket Source to Ticket Create 2022-11-08 19:31:57 +00:00
Peter Rotich
a4e36d399c mailparse: Handle Mail Parse Errors Gracefully
On mail parse error, osTicket typically leaves the email alone and reports it
as a fetch failure - which results in multiple fetch attempts until an admin
manually moves the email out of the Fetch Folder. Most parse errors are caused
by poorly formatted mime message (email) that failes to decode.

This commit adds ability to create a ticket even when osTicket encounters mail
parse error. The entire raw email is attached to the ticket as message/rfc822
(.eml) attachment.
2022-11-08 19:31:57 +00:00
Peter Rotich
75c5cfe91e fetcher: Delayed Mailbox Expunction
As part of post-processing options, when fetching emails, osTicket can archive
(move) or delete (remove) a mail message from the server. When using mail
storage backend like IMAP - both operations requires `expunge` command to truly
commit the changes to the mail server.

That's all good and well, except Laminas Mail (our darling upstream lib)
expunges the mailbox in real-time when a message is moved or deleted. This
creates a problem for osTicket because **expunging causes message sequence
numbers to reset at the server end during the session!!** Which in turn causes
mismatch when we process the next message in the queued sequence. Additionally,
expunging on every message operation cases unnecessary traffic to the mail
server when every second counts.

To resolve the issue - until we switch to using `UIDs` -  this commit simply
bypasses Laminas Mail IMAP's removeMessage function so we can skip expunging on
message move/remove operations. osTicket expunges the mailbox at the end of
fetch session.
2022-11-08 19:22:05 +00:00
Peter Rotich
320981df67 fetcher: Improved Error / Warning Messages and Comments 2022-11-08 19:22:05 +00:00
JediKev
cd3c101246 Merge pull request #6355 from protich/issue/Sendmail+Windows
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-08 12:41:10 -06:00
JediKev
40f16dd2d6 Merge pull request #6350 from protich/issue/SMTP+QUIT
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-08 12:31:11 -06:00
JediKev
6ab019c161 Merge branch '1.17.x' into issue/SMTP+QUIT 2022-11-08 12:29:49 -06:00
JediKev
2b2f0a524e Merge pull request #6344 from protich/feature/fetchorder
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-08 12:25:18 -06:00
JediKev
78c4e5c617 Merge pull request #6307 from protich/issue/setup-session
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-08 12:23:31 -06:00
JediKev
9ab605e699 Merge pull request #6303 from protich/issue/3366
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-08 12:20:00 -06:00
JediKev
c02dce7468 Merge pull request #6302 from protich/feature/mail+plain
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-08 12:14:32 -06:00
JediKev
8c52e042f7 Merge pull request #6301 from protich/issue/mailer++
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-08 12:13:47 -06:00
Peter Rotich
bfccc82057 Merge pull request #6352 from JediKev/issue/mta-typo
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-11-08 11:28:28 -06:00
Peter Rotich
75e111cecc Merge pull request #6308 from JediKev/issue/file-upload-size
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-11-08 11:27:46 -06:00
Peter Rotich
39d7625882 Misc. Typo Fix 2022-11-08 17:23:58 +00:00
Peter Rotich
ac3855a4b5 sendmail: Strip "To" and "Subject" headers
This commit removes "to" and "subject" headers before headers are prepared and
passed to MTA. It's necessary since the headers in question are set directly
via PHP mail() function - leaving them results in duplicate headers.

This is a temp fix needed for Windows installs until we upgrade to the latest
version of Laminas Mail which already has the fix. The version we use currently
doesn't strip the headers on Windows.
2022-11-08 03:59:25 +00:00
JediKev
03eeb8e2b9 issue: MTA Typo
This addresses a small typo (6351) where `l` was being added to each
email string for the Default MTA setting. This removes the `l` so the
emails are displayed properly.
2022-11-07 16:40:21 -06:00
Peter Rotich
977cf9aafb typo: Fix Typo 2022-11-07 22:07:14 +00:00
Peter Rotich
dbeae227de SMTP: Dont send 'QUIT' on __destruct()
This commit attempts to avoid errors / exceptions that cannot be managed from PHP
userspace when an SMTP object is destructed. It also adds a connection timeout
of 5 minutes so old connections can be recreated.
2022-11-07 20:21:45 +00:00
JediKev
45f554adf6 issue: File Upload Size
This addresses an issue where if the Issue Details field or any custom
FileUpload field don't have `configuration` set any file upload to such
fields will fail with `File too large` error. This is due to the
`configuration` not being set so when we check the uploaded file size to
the allowed file `size` from the field's config via `$config['size']` it
fails. This adds a check for the `size` option in the `$config` array
and if not set or set to NULL then we will default to the system setting
`Agent Maximum File Size`.
2022-11-07 13:50:12 -06:00
JediKev
c145b2d8fd Merge pull request #4186 from JediKev/issue/system-timezone-var
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>, protich <peter@osticket.com>
2022-11-07 11:53:08 -06:00
Peter Rotich
24338c2b5d fetcher: Max Fetch vs. Mail Box Count
This commit addresses a logic bug introduced in commit f1639d4123
2022-11-06 21:45:15 +00:00
JediKev
bc017ecdb5 Merge pull request #6343 from protich/issue/mailfetch_errors
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-03 16:42:16 -05:00
JediKev
268a246b19 Merge pull request #6342 from JediKev/issue/miscellaneous-fixes
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-03 16:41:49 -05:00
JediKev
ca02831c08 Merge pull request #6339 from protich/issue/email-status+upgrader
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-03 16:41:00 -05:00
JediKev
1b4ec3d0e8 Merge pull request #6333 from JediKev/issue/urldecode-dispatcher
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-03 16:40:48 -05:00
JediKev
a1977d9265 Merge pull request #6332 from JediKev/issue/plugin-config-item-exists
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-03 16:40:11 -05:00
JediKev
5c76bd8183 Merge pull request #6329 from JediKev/issue/information-sectionbreak-fields-on-edit
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-03 16:38:51 -05:00
JediKev
7899ae023a Merge pull request #6327 from JediKev/issue/favicons-login-headers
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-03 16:38:32 -05:00
JediKev
5784621fec Merge pull request #6324 from JediKev/issue/schedules-stops_on
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-03 16:38:05 -05:00
JediKev
cecab4a3c4 Merge pull request #6316 from aydreeihn/issue/assign-perm
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-11-03 16:36:42 -05:00
JediKev
5e970061b5 issue: No Ticket For checkStaffPerm()
This addresses an issue where there may be no `$ticket` available which
makes the `checkStaffPerm()` check fatally fail. This adds a check for
`$ticket` before checking the permissions. If no Ticket exists then we
set `Access Denied` error.
2022-11-03 16:09:59 +00:00
JediKev
c2a15b64c7 update: Static isValidFile()
As the name suggests this converts the `FileUploadField::isValidFile()`
method to static. This also updates the relevant calls to static format.
Lastly, this removes the check for `function_exists('exif_imagetype')` as we
now define this if it doesn't exist.
2022-11-03 16:09:59 +00:00
JediKev
9c280b49d6 searchable: Length Param
This adds a minor improvement to the `Format::searchable()` method by adding
a new param called `$length` that will allow you to pass in a specific
length to ensure the text is less than or equal to the supplied length. If
the text length is greater than the supplied length then it will return
`NULL` (ie. not searchable).
2022-11-03 16:09:59 +00:00
JediKev
3568418939 issue: Position CSS
This addresses an issue where some emails contain CSS `position` attributes
that can royally mess up the Ticket View. This adds REGEX to the
`Format::safe_html()` method that will remove any `position` attributes to
avoid rendering issues. This also adds a `position` attribute to the Thread
Entry div to ensure proper rendering.
2022-11-03 16:09:59 +00:00
JediKev
e036fd9a1e issue: Malformed Headers
This addresses an issue where some emails have malformed headers like `From:
"User Name" <email@domain.tld` which causes issues when processing.
This adds a check for malformed headers and if malformed then it will fixup
the header to prevent processing issues.
2022-11-03 16:09:59 +00:00
JediKev
5002bef9f3 issue: SelectionField Typeahead Validation
This addresses an issue where when validating a SelectionField's typeahead
value if there is no `$entry` the check will fail. This ensures `$entry`
exists and is set before using it in the subsequent checks.
2022-11-03 16:09:59 +00:00
Peter Rotich
f1639d4123 fetcher: Mail Fetch Order
osTicket currently fetches messages in LIFO order meaning the most recent messages get
fetched first. This was intentional to avoid possible stalemate when fetched
messages are left in the inbox (Do Nothing Option). However, this can creates
out of order threading issues since subsequest replies can be fetched first.

This commits addresses the issue by changing the logic so we can fetch older
messages first (FIFO order) as long as the numbers of the emails in the folder
being fetched are less than maximum messages to process at a time.
2022-11-03 10:13:56 +00:00
Peter Rotich
c92006e638 mail: moveMessage Return Value
This commit makes sure we're returning the correct result (boolean) when moving
a message. Laminas Mail (upstream) doesn't return a value when moving a message
- success is assumed unless we catch an exception.
2022-11-03 09:42:55 +00:00
Peter Rotich
22edf2b42e mailfetcher: Fetch vs. Archive Folders
This commit removes ambiguity by making it clear which folder we're referencing.
2022-11-03 07:56:56 +00:00
Peter Rotich
384aad1365 mailfetcher: Catch & Log Post Create Errors
This commit adds a try/catch to catch possible exceptions that might happen
post ticket create. This is necessary so we can isolate problematic emails from
segfaulting the entire process (fatal error) or cause it to skip expunging the
mailbox.

The commit also adds debug logging for such errors / exceptions.
2022-11-03 07:56:56 +00:00
JediKev
50bc1a2133 issue: exif_imagetype()
This addresses an issue where if someone doesn't install the PHP `exif`
extension, subsequent calls to `exif_imagetype()` returns `undefined`
errors. This checks to see if the method is defined and if not defines it.
2022-11-02 20:23:02 +00:00
Peter Rotich
aee25d69a5 upgrader: Email Account Status
This commit addresses an issue where we mistakenly disabled email account
status when upgrading to 1.17.x from older versions.

The change makes sure the status is maintained based on what's set on
hashtable.
2022-10-31 21:06:38 +00:00
Peter Rotich
db913bd385 cron: Cleanup Expired Sessions
This commit fixes cron session cleanup to use the new session model
2022-10-28 02:17:44 +00:00
Peter Rotich
c2be4ebf02 Misc. Fixes 2022-10-27 16:23:15 +00:00
Peter Rotich
f2c018409e session: Add onEvent Callback Support
This commit adds ability for session backends to report on certain events,
allowing us to keep app logic out of the session library.

The only event supported at the moment is "onClose"  - triggered right before
the session commits the session data.
2022-10-27 14:59:34 +00:00
Peter Rotich
da6673667e Add Basic Checks for Database Session Record
Make sure User Agent stays same and session id len is 32
2022-10-26 20:42:40 +00:00
Peter Rotich
9ab5b4f18f session: Regenerate session based on Max Idle Time
osTicket renegerates session id 10 seconds after login and that's it. This
commit add a timer to regenerate session id based on user's set max idle timeout
regardless of activity. If session timeout is disabled then it defaults to
every 24 hrs.
2022-10-26 20:17:59 +00:00
JediKev
c00b8e879a issue: urldecode Dispatcher
This addresses an issue where if using a webserver that encodes URLs by
default (like NGINX) some of the AJAX URLs will not resolve. This
ensures the URL is decoded with `urldecode()` before matching and
dispatching.
2022-10-26 14:47:56 -05:00
JediKev
fdf9e4127a issue: Plugin Config Item Exists
This addresses an issue where the Audit Log plugin shows Ticket Views in
the Audit Logs and shows the setting as enabled even when you disable
and save via plugin config. This is due to the current
`Config::exists()` method using the config `->get()` method and checking
the return value to determine `true` or `false`. This is problematic,
especially when values can be `NULL` or `0`. This updates the
`Config::exists()` method to check if the key exists in the session, in
the config array, or in the defaults. If the key does exist then we
return `true` otherwise we return `false`.
2022-10-26 14:45:55 -05:00
Peter Rotich
e2e6c0f436 session: Make Session Name Config Option
This commit adds session name as an option in config file. Also it
adds information about chaining session storage backends.
2022-10-26 19:37:36 +00:00
Peter Rotich
47df2b4409 session: use osTicket\Session Backends
This commit should have probably been broken into smaller commits but the
wholesale change was required since it's an overhaul of how osTicket handles
sessions.
2022-10-26 01:18:21 +00:00
Peter Rotich
f58e1c5d62 session: osTicket Session Backends
This commit adds unified & abstract osTicket Session Handlers / Storage
Backends to make it easy to add and extends storage backends.
2022-10-26 00:21:03 +00:00
JediKev
7b60c57653 issue: Information and SectionBreak Fields On Edit
This addresses an issue where Information and SectionBreak Fields do not
display on Ticket edit for Users and Agents. This is due to no
visibility option for Editable for these field types. Since these field
types don't have Editable visibility when we check for
`isEditableTo[Staff|Users]()` it returns false. This overwrites the two
methods for both field types so that they are visible on Edit if the
field is visible to the respective user-type (User/Agent).
2022-10-24 10:26:13 -05:00
JediKev
61655ec608 issue: Favicons Login Headers
This addresses a small issue where Favicons weren't appearing on the
Staff Login Page. This is due to the Staff Login Page using a different
header than other pages. This adds the Favicons to the Staff Login Page
Headers so that they appear correctly.
2022-10-19 14:48:28 -05:00
JediKev
e2dcbb6991 issue: Schedules stops_on
This addresses a small issue where we were calling `setTimestamp()`
instead of `getTimestamp()` which causes a Fatal Error. We didn't notice
this in previous versions due to pre-PHP 8 it was only a Warning not a
Fatal Error. This updates the line to call the correct method which
avoids any fatal errors.
2022-10-18 14:25:24 -05:00
aydreeihn
b4732b0523 Issue: Agent Permission to Assign Tickets
When we do $thisstaff->getDeptAgents, we are trying to get active agents $thisstaff can assign tickets to. The query done for this groups records in the staff_dept_access table by staff_id, so when we try to do getRoles() for $thisstaff, the only role we can get from staff_dept_access is the first record for $thisstaff.

This commit unsets the dept_access relation so that it can be requeried to get all staff_dept_access records for $thisstaff.
2022-10-13 17:09:55 -05:00
Peter Rotich
b6b63c9916 setup: Use Bootstrap:ini() to initialize session
This commit centralizes sessions init by defering it to Bootstrap:ini() -
when setup scripts needs to start an independent session (e.g cli installer)

Centralized session init is also important because, going forward, it will make it
easy to make changes in one place e.g when changing error_reporting setting
application-wide.
2022-10-10 22:24:44 +00:00
Peter Rotich
48e436f002 setup: Use Bootstrap:ini() to initialize session
This commit centralizes sessions init by defering it to Bootstrap:ini() -
when setup scripts needs to start an independent session (e.g cli installer)

Centralized session init is also important because, going forward, it will make it
easy to make changes in one place e.g when changing error_reporting setting
application-wide.
2022-10-09 22:06:44 +00:00
Peter Rotich
b943a95536 cli: Remove inline noop session handler
This commit removes noop session hadler set by manage.php for cli utils - cli utils generally don't require sessions and are disabled by default.
2022-10-09 21:53:55 +00:00
Peter Rotich
ced0ef4da3 session: Add Noop Session Backend
osTicket support uploading your own logos as well as login backgrounds and
calls logo.php (on both panels) to fetch the image in question. Previously we
didn't care about session data so session handler was set to an inline noop
function as a way to bypass starting a "real" session. The problem with the
approach is; if osTicket is hosted behind a proxy with multiple web severs in the backend,
the proxy needs to send all requests to the same server for specific user
session. To do so session name (OSTSESSID) is used to route requests from the same session
to the same backend server.

This commit adds a noop session backend allowing us to start a session while
still ignoring data writes. Noop backend is used when DISABLE_SESSION is
defined and set to false.
2022-10-09 20:46:40 +00:00
Peter Rotich
a9b5749656 email: Split Auth updates & getters into individual routines
This commit doesn't add anything useful other an splitting Auth updates & getters into individual routines per backend.
2022-10-09 05:24:01 +00:00
Peter Rotich
5efd9f14dd email: Make sure Email Account is Active 4realz!
This commit adds a check to make sure an Email Account has valid credentials
when determining if the account is active. It's necessary so we can detect
deleted credentials or expired refresh token in the case of accounts with
oauth2 backends.
2022-10-09 05:20:23 +00:00
Peter Rotich
b0e5ac83f2 smtp: Use proper name when saying What's Up (HELO/EHLO)
This commit addresses an issue  where outgoing email can get rejected when the
target mail server is tight-assed - requiring a valid host when saying
helo/ehlo.

We were setting the name to null which resulted in localhost being used as the
name. Now we're asking the OS to give us the proper hostname of the server -
hopefully the server admin has the correct hostname and reverse-lookup setup
correctly for better deliverability.
2022-10-09 00:04:37 +00:00
Peter Rotich
7c1b97b306 mail: Add plain:// encryption scheme hint
When configuring an email, starting with osTicket v1.17, the adminstrator can
hint to the desired encryption when using non-stantard ports by prefixing the
`hostname` with an encryption scheme  e.g `tls://` or `ssl://`. Using hints is
much better and less confusing than the old ways of presenting a user with
`IMAP` and `IMAP + SSL` as protocol selections - when infact we could use the
port number, in the backend, to determined the encryption.

This commit adds support for the opposite  - by allowing an admin to force
unencrypted connection even when using standard encryption ports by simply
adding `plain://` as the scheme hint.
2022-10-08 22:14:10 +00:00
Peter Rotich
23a80592e9 mail: Message-Id
This commit adds a routine to get Message-Id and also adds a check for an
invalid Id when trying to set a Message Id.

Please note that invalid ID are silently ignored since the upstram library
generates one.
2022-10-08 19:47:58 +00:00
Peter Rotich
845e500844 mailer: Spoofing & Originator
Spoofing: When an SMTP Account **DOES NOT** allow spoofing then set the Sender
(Envelope FROM) to the account's email address and leave the Header FROM as
set

Originator: When an SMTP account send fails - we now reset the Originator /
Sender to the original From Address, incase spoofing setting changed the
Sender

* Spoofing - when an SMTP Account does NOT allow spoofing then set the Sender
(Envelope FROM) to the account address and leave the Header FROM as set.

* SetOrinator - When an SMTP send mails we now reset the Originator / Sender to the original
From Address just incase spoofing setting had changed the Sender.
2022-10-08 19:42:59 +00:00
Peter Rotich
0564438c9c Merge remote-tracking branch 'upstream/1.17.x' into 1.17.x 2022-10-06 17:52:46 +00:00
Peter Rotich
7ef7dec03c Merge remote-tracking branch 'upstream/develop' into 1.17.x 2022-10-06 17:52:36 +00:00
JediKev
03f8813348 Merge pull request #6292 from JediKev/cli/user-import
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>, protich <peter@osticket.com>
2022-10-06 12:32:34 -05:00
JediKev
1301db4935 Merge pull request #6297 from JensEB/patch-1
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>, protich <peter@osticket.com>
2022-10-06 12:27:52 -05:00
JediKev
c431d26bb4 Merge pull request #6296 from protich/issue/Email_Message-Id
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>, protich <peter@osticket.com>
2022-10-06 12:23:03 -05:00
Peter Rotich
60c8c8841e mailer: Add Sender Header
Sender is equivalent to -f from@address in mail() interface which get used as
envelope FROM address - this is important when header FROM is spoofed.
2022-10-06 16:57:39 +00:00
Peter Rotich
020fa0d3db mailer: Lint Fix
$type is a protected static upstream without a getter!
2022-10-06 16:05:12 +00:00
Peter Rotich
2223877c6a mailer: Add InReplyTo Header Interface
In-Reply-To: Unbreak My Soul
2022-10-06 03:46:08 +00:00
Peter Rotich
00981554be mailer: Add References Header
Use References Header to add references instead of using Generic Headers which might break due encoding
2022-10-06 03:46:08 +00:00
Peter Rotich
aaee6b0fde mailer: Add Return-Path Header Interface
This commits adds Return-Path Header interface to osTicket\Mail\Message
2022-10-06 03:45:59 +00:00
JensEB
ccd8445e97 Update class.mail.php 2022-10-05 20:18:54 +02:00
JensEB
b84547b0b6 POP3 Configration can not saved
First error: abstract functions can not be private. It have to be protected or public...

Second error: the pop3 login function doesn't respond anything on successful login. This will be return false in MailBoxProtocolTrait::basicAuth()
2022-10-05 14:17:09 +02:00
Peter Rotich
c08a8a8a21 mailer: Make Headers Valid Again
Message-Id: osTicket uses encoded message Id to thread emails on reply.
Laminas Mail, the new backend we're mangles up the Message-Id when set as a
generic header. This commits corrects the issue by using MessageId object -
making sure it is ASCII encoded.

Date: Date is auto-added upstream - so we're removing it from Mailer to
avoid double header issues - but if need be, you can overwrite it by setting
your own fake date!
2022-10-05 05:49:06 +00:00
JediKev
4c43e46f68 cli: User Import
This addresses issue 6291 where importing users from CSV via command
line causes a fatal error. This is due to attempting `fclose()` on a
stream that has already been closed. The CsvImporter `__descontruct()`
method already closes the stream so when we get to the end of the import
and attempt to call `fclose()` in the CLI method, it causes a fatal
error as the stream is no longer valid (as it's already been closed).
This adds a check to see if the stream is a valid resource and if so
then we close it, otherwise we skip and continue (as it's already
closed).
2022-10-03 15:34:55 -05:00
Peter Rotich
6932ea84cf Merge remote-tracking branch 'upstream/develop' into develop 2022-09-27 20:54:54 +00:00
JediKev
d729810a96 oops: Fix System Timezone Variable
This addresses an issue where people are getting the incorrect time on
tickets even though they have all the timezones set correctly. The
current MySQL variable the system uses to determine the timezone is
`system_time_zone`. This variable is set by the server and sometimes
cannot be updated properly (or cannot be updated at all). This changes
the variable from `system_time_zone` to `time_zone` so if in the event
the system returns the incorrect time and they can’t update the server
timezone properly, they can just change the `@@global.time_zone` and
`@@session.time_zone` MySQL variables and should get the correct time.
2018-07-17 10:16:34 -05:00
3125 changed files with 74656 additions and 232142 deletions

2
.gitignore vendored
View File

@@ -7,6 +7,8 @@ setup/cli/modules/importer
.vagrant
._.DS_Store
Vagrantfile
# VS Code workspace files
*.code-workspace
# Staging directory used for packaging script
stage

View File

@@ -22,13 +22,14 @@ easy to setup and use. The best part is, it's completely free.
Requirements
------------
* HTTP server running Microsoft® IIS or Apache
* PHP version 8.0
* PHP version 8.2 - 8.4 (8.4 recommended)
* mysqli extension for PHP
* MySQL database version 5.5
* MySQL database version 5.5 (or greater)
### Recommendations
* gd, gettext, imap, json, mbstring, and xml extensions for PHP
* APC module enabled and configured for PHP
* ctype, fileinfo, gd, gettext, iconv, imap, intl, json, mbstring,
Zend OPcache, phar, xml, xml-dom, and zip extensions for PHP
* APCu module enabled and configured for PHP
Deployment
----------
@@ -58,33 +59,9 @@ Upgrading
---------
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 tarball from either
the osTicket [github](http://github.com/osTicket/osTicket/releases) page
or from the [osTicket website](https://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
source code to your server.
Any way you choose your adventure, when you have your codebase upgraded to
osTicket-1.7, visit the /scp page of you ticketing system. The upgrader will
be presented and will walk you through the rest of the process. (The couple
clicks needed to go through the process are pretty boring to describe).
### Upgrading from v1.6
**WARNING**: If you are upgrading from osTicket 1.6, please ensure that all
your files in your upload folder are both readable and writable to your
http server software. Unreadable files will not be migrated to the
database during the upgrade and will be effectively lost.
After upgrading, we recommend migrating your attachments to the database or
to the new filesystem plugin. Use the `file` command-line applet to perform
the migration.
php manage.php file migrate --backend=6 --to=D
View the UPGRADING.txt file for other todo items to complete your upgrade.
osTicket codebase before embarking on an upgrade. Please review our [Upgrade
Guide](https://docs.osticket.com/en/latest/Getting%20Started/Upgrade%20and%20Migration.html)
or the [UPGRADING.txt file](UPGRADING.txt) for upgrade instructions.
Help
----
@@ -101,7 +78,7 @@ the feature is published in your fork, send a pull request to begin the
conversation of integrating your new feature into osTicket.
### Localization
[![Crowdin](https://d322cqt584bo4o.cloudfront.net/osticket-official/localized.png)](http://i18n.osticket.com/project/osticket-official)
[![Crowdin](https://badges.crowdin.net/osticket-official/localized.svg)](https://crowdin.com/project/osticket-official)
The interface for osTicket is now completely translatable. Language packs
are available on the [download page](https://osticket.com/download). If you
@@ -122,18 +99,19 @@ file for the gory details of the General Public License.
osTicket is supported by several magical open source projects including:
* [Font-Awesome](http://fortawesome.github.com/Font-Awesome/)
* [HTMLawed](http://www.bioinformatics.org/phplabware/internal_utilities/htmLawed)
* [jQuery dropdown](http://labs.abeautifulsite.net/jquery-dropdown/)
* [jsTimezoneDetect](http://pellepim.bitbucket.org/jstz/)
* [mPDF](http://www.mpdf1.com/)
* [PasswordHash](http://www.openwall.com/phpass/)
* [PEAR](http://pear.php.net/package/PEAR)
* [PEAR/Auth_SASL](http://pear.php.net/package/Auth_SASL)
* [PEAR/Mail](http://pear.php.net/package/mail)
* [PEAR/Net_SMTP](http://pear.php.net/package/Net_SMTP)
* [PEAR/Net_Socket](http://pear.php.net/package/Net_Socket)
* [PEAR/Serivces_JSON](http://pear.php.net/package/Services_JSON)
* [Font-Awesome](https://fontawesome.com/)
* [HTMLawed](https://www.bioinformatics.org/phplabware/internal_utilities/htmLawed)
* [jQuery dropdown](https://labs.abeautifulsite.net/jquery-dropdown/) (Project Deleted)
* [jsTimezoneDetect](https://pellepim.bitbucket.org/jstz/)
* [laminas-mail](https://github.com/laminas/laminas-mail)
* [mPDF](https://github.com/mpdf/mpdf)
* [PasswordHash](https://www.openwall.com/phpass/)
* [PEAR](https://pear.php.net/package/PEAR)
* [PEAR/Auth_SASL](https://pear.php.net/package/Auth_SASL)
* [PEAR/Mail](https://pear.php.net/package/mail)
* [PEAR/Net_SMTP](https://pear.php.net/package/Net_SMTP)
* [PEAR/Net_Socket](https://pear.php.net/package/Net_Socket)
* [PEAR/Serivces_JSON](https://pear.php.net/package/Services_JSON)
* [php-gettext](https://launchpad.net/php-gettext/)
* [phpseclib](http://phpseclib.sourceforge.net/)
* [Spyc](http://github.com/mustangostang/spyc)
* [phpseclib](https://phpseclib.sourceforge.net/)
* [Spyc](https://github.com/mustangostang/spyc)

View File

@@ -1,5 +1,33 @@
Welcome to osTicket 1.7
=======================
Upgrading osTicket
==================
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 tarball from either
the osTicket [GitHub](https://github.com/osTicket/osTicket/releases) page
or from the [osTicket website](https://osticket.com/download). 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
source code to your server.
Any way you choose your adventure, when you have your codebase upgraded,
visit the /scp page of you ticketing system. The upgrader will
be presented and will walk you through the rest of the process. (The couple
clicks needed to go through the process are pretty boring to describe).
### Upgrading from v1.6
**WARNING**: If you are upgrading from osTicket 1.6, please ensure that all
your files in your upload folder are both readable and writable to your
http server software. Unreadable files will not be migrated to the
database during the upgrade and will be effectively lost.
After upgrading, we recommend migrating your attachments to the database or
to the new filesystem plugin. Use the `file` command-line applet to perform
the migration.
php manage.php file migrate --backend=6 --to=D
Some tasks are better left to a system administrator rather than a mindless
upgrade script. These are those remaining things that we'd rather you take
care of:

View File

@@ -1,3 +1,232 @@
osTicket v1.18.4
================
### Security
* security: Latest Patches 06/2026 (52c366f, 5afdf54, c54a6ac, 1e39bf1, feccb6a, 6eb6b98, 078516e, 98abb05, e52e010, fd96bba, 7bbd8ab, ba6217a, 580e1c8, b535782, 5963797, d590a97, eaebe01, b4cc092, d457c14, 5600f94, 5ff9795, 119cefe, b4ede88, 2a0c388, 6558b33)
osTicket v1.18.3
================
### Enhancements
* mPDF: Upgrade To v8.2.7 (39cdd2b1)
* htmLawed: Upgrade To 1.2.15 (877adf56)
### Security
* security: Latest Patches 01/2026 (c646c8cb, d1b634a9, 6852e719, 8db4d377, 1552cfa3, d832f243)
osTicket v1.18.2
================
### Improvements
* OAuth2: Show Scopes and Enforce Strict Mode (a77cf535, 390555db)
osTicket v1.18.1
================
### Improvements
* Update upgrade.php (9fd83eba)
* Update upgrade.inc.php (8c8a7fd1)
* update: PHP Requirements 1.18.x (1c0c670b)
* CLI: Make sure manage util can be executed via CLI (0caf5864)
* Update raphael-min.js to 2.3.0 (d4aeada1)
osTicket v1.18
==============
### Enhancements
* update: Laminas-Mail (66fa10af, bb45d37a)
### Improvements
* Database: Change Plugin Name to varchar(255) (aac546d0)
* update: MAJOR_VERSION (3eac42e5)
osTicket v1.17.8
================
### Security
* security: Latest Patches 06/2026 (52c366f, 5afdf54, c54a6ac, 1e39bf1, feccb6a, 6eb6b98, 078516e, 98abb05, e52e010, fd96bba, 7bbd8ab, ba6217a, 580e1c8, b535782, 5963797, d590a97, eaebe01, b4cc092, d457c14, 5600f94, 5ff9795, 119cefe, b4ede88, 2a0c388, 6558b33)
osTicket v1.17.7
================
### Enhancements
* mPDF: Upgrade To v8.2.7 (39cdd2b1)
* htmLawed: Upgrade To 1.2.15 (877adf56)
### Improvements
* join cdata table earlier (81ec2562)
* issue: Content Management Errors (2a4fe6ad)
* mail: SMTPUTF8 Backwards Compatibility (05b87199)
* issue: SVG MIME Type (a6309e22)
* Fix Banlist permission check (a7f14bf2)
* issue: POP OAuth2 Error REGEX (10f38bd0)
* issue: Kazakh Flag CSS (624c7171)
* Fixing Argentinian flag (924380a9)
* Update class.setup.php (8b6a0f05)
* Fixes for Implicitly marking parameters as nullable (573e8496, ca13d888)
* issue: Date Column Interval (1e38901e)
* issue: PGP + S/MIME Mail Parsing (9aff6b03)
* issue: Trim User Name (0fbafda1)
* issue: Microsoft UTF-8 BOM (5074c032)
* issue: Empty Dept Error On Topic Update (f1ba934b)
* Fix typo in 'Add New Instance' title (ae434418)
* Update class.file.php (1425fe1e)
* issue: Regex OR Operator (331ca48d)
* Revert "issue: Guest Session TTL" (c3819e4d)
### Security
* security: Latest Patches 01/2026 (c646c8cb, d1b634a9, 6852e719, 8db4d377, 1552cfa3, d832f243)
osTicket v1.17.6
================
### Improvements
* Fix PR #6283: DB column value of "local backend" is 'client' and not 'local' (7805cd94)
* i18n: Tasks Department ID Missing (00bc6b1b)
* issue: Checkbox Template Variable (09de9587)
* Fixed mailfetch error message (cd4717a1)
* issue: fix square characters being printed when printing tickets that's using languages like Thai (872a6492, 25844034)
* issue: Position Styling.. Again (7f03a64b, b843fb15)
* mysqli: Multi-Host Failure (0a8475fb)
* issue: Task Last Respondent (5277c7ab)
* i18n: Schedules getDays() (f2facda3)
* issue: Referred Closed Tickets (a7b0711d)
* php: Update Prerequisites (d331a44c)
* i18n: Lang URL Param (fece6ff7)
* mysql: Update Minimum Version (317ab967)
* Fix ajax path for AddRemoteCollaborator (6df6cd98)
* Fix staff mobile field class (f8455c82)
* patches: Latest 05/28/2024 (2d65cb77, 36b4c94f, 854cf47c, d9fe3ada)
* upgrade: Laminas-Mail (2.25.1) (c0a74162, 939a278c, f658268d)
* hotfix: Fix getRawEmail() (c26d840c, 1eb71197)
* issue: Linked Icon annotation (6c096cce)
* fix(email): fix case sensitivity in "Action" header retrieval (07843598)
* Upgrade mimeDecode.php from v1.48 to v1.5.6 2016-08-28 (cbd78ac8)
* mailer: sendmail() Failure (14e2057b)
* php: 8.3 Support (a3931f46, b38db372, fea5e1e0, 387a1c04, 136d372e)
* issue: Ticket Relations (9426b2dd)
* issue: Basic Search Selector (74b8bd0e)
* mail: Reapply Memory Optimization (eaaa64d9, ab76ffe8)
* php: iconv Recommendation (33ecc3a0)
* Ignore VS Code Workspace Files (0da2e0e0)
* issue: Email Remote Backend Name (b54dd584)
* issue: Safari Response Content Disposition (64a5df68)
* api: DueDate and Other Errors (14814ae2)
* issue: Multiselect List Export (f3bf8553)
* issue: Image Annotation (aeeb2850)
* user: List Import (fec70c4a)
* issue: Latest SQL Warnings (9bdfde5a)
* fields: Variable Name Validation (0724d1ad)
* issue: Transfer Empty Comments Var (ed87b257)
* update: Composer symfony/process (59f25918)
* issue: Client ACL Staff File Download (8255b2e7)
* update: PHP Deprecations & Warnings (84c14ace)
* issue: User Lookup Umlaut (504b0bfb)
* issue: Department Field User Import (5c2b6a5d)
* Fix force_https auto-enabling on settings page (3805bbfd)
* queue: getTotal Incorrect Counts (4f137dc2)
* issue: Referred Tickets Incorrect Queue Counts (b42cad6f)
* php: 8.4 Support (a4c0f674, b4938b99, 5e5a9ff5, fb9a39ba)
* issue: Canned Response Access (b930a68b)
* issue: Excessive Fetching Errors (c546a167)
* issue: Plain-text Base64 (216ded32)
* i18n: Crowdin API v2 (0ec6670d)
* issue: iFrame Logins (3a5da66b)
* issue: mimeDecode .eml Attachments (7fc3d8c2)
* patches: Latest 11/18/2024 (416b548b, 245e7554)
* Revert part of 0784359 commit (ec76a203)
### Security
* security: Latest Vulns 01/2025 (193f5fe0, ab6672fa)
osTicket v1.17.5
================
### Improvements
* update: README.md PHP & Laminas (4baf9dc8, 555c4a74, 88294e2d, 30a4a6e9)
* issue: putenv() Disabled (3c0d5f47)
* i18n: Update Signing (98706274, 776c0f27)
* i18n: Flags Position & Codes (3ee11112, 45f5b5b4)
* mailer: Undefined SmtpAccount::getName() (560a4f1d)
* issue: SystemSessionHandler (7e17daa6)
* issue: Status List Overflow (1deee342)
* faq: Category Notes Images (ccca0f59)
* patches: Latest 10/03/2023 (65e12297, 48e8501d, a8d42a9d, 4475e03e, bd034712, 64b8c81d, 070eefcb, 1775ce75)
* mail: NoValidateCert (d3c140ce)
* issue: Choice/Selection Field Searches (00e22fad)
* stubs: Add stubs to pass lint test (895c7236)
* issue: Canned Response Inline Images (a503c160)
* format: Viewable Images Bug (2a412883)
### Security
* issue: CSP Headers (6228f640)
* security: Latest Vulns 09/2023 (04f4e611, 88a87a33, c4ad48de, 37cf8350)
* issue: User Account Creation (777e6f0b)
osTicket v1.17.4
================
### Improvements
* issue: Thread Entry Actions z-index (215a0ce2)
* oauth2: Strict Matching Bug (e014ffd2)
* Make string translatable (1105cde7)
* issue: Inline-Images Canned Responses (4493b126)
* issue: Remove Old Login Code (e17ad463)
* issue: i18n Audit Exports (45dd7c4f)
* Bug: Custom File Upload Field Config (6371269a, 7283ac81, 902b5d35)
* issue: Duplicate User Copy/Paste Import (b304cdb)
* issue: Relocate Typeahead JS/CSS Files (62cd406)
* issue: strftime() Deprecation (3fe132c4)
* jquery: Update To 3.7.0 (ffa23da)
* issue: glob() Empty Array (c64a2611)
* issue: Email Only Attachment (9e45f3fa)
* installer: Change Email (a11aee29)
### Security
* security: Latest Vulns 06/2023 (86c2ba02, 69244175, 73b997a, ae37925, e4bfb00)
osTicket v1.17.3
================
### Enhancements
* oauth2: Strict Matching (7275d67b, b7718be4, ca913ba2, cb3625e8, f531718a, c0cd8b16)
* OAuth2 Resource Owner (5c239adb)
### Improvements
* session: Regenerate Session Id (d5853245)
* fetcher: Mail Fetcher / Parser Error Handling (d4d9c424, 0a4498b2)
* mysql: Support Sidecar Database Proxies (246aaa4d)
* Misc. Fixes (e6beeb9b, 24fd5075, eac9960a, 10af29a5)
* fetcher: Default Department (f410d4ec)
* issue: LDAP Multi-Instance Fatal Error (f6486044)
* issue: TicketDenied errno (6de7cf4c)
* session: User Logged Out (00d409d3)
* Uncaught Error: Call to a member function getId() on bool (af83896d)
* Mailer: Inline Images (7efbdfd2, 8815d087, 64a8abf3)
osTicket v1.17.2
================
### Improvements
* Fix old reference to SessionData (cf8b8ce7)
* Email Misc. Fixes (de620fe6, c19a5f03)
* session: Memcache Max TTL (ea3e03d2)
* mailer: Use Namespaced Mailer (133585f8)
osTicket v1.17.1
================
### Enhancements
* mail: Add plain:// encryption scheme hint (7c1b97b3, 39d76258)
### Improvements
* mailer: Make Headers Valid Again (c08a8a8a, aaee6b0f, 00981554, 2223877c, 020fa0d3, 60c8c884)
* POP3 Configration can not saved (b84547b0, ccd8445e)
* fetcher: Errors Handling & Logging (384aad13, 22edf2b4, c92006e6)
* upgrader: Email Account Status (aee25d69)
* issue: Plugin Config Item Exists (fdf9e412)
* mailer: Improvements & Enhancements (845e5008, 23a80592, 977cf9aa)
* smtp: Use proper name when saying What's Up (HELO/EHLO) (b0e5ac83)
* email: Make sure Email Account is Active 4realz! (5efd9f14, a9b57496)
* Session++ (ced0ef4d, b943a955, b6b63c99, f58e1c5d, 47df2b44, e2e6c0f4, 9ab5b4f1, da667366, f2c01840, c2be4ebf, db913bd3)
* setup: Use Bootstrap:ini() to initialize setup dir sessions (48e436f0)
* issue: Plugin Config Item Exists (fdf9e412)
* upgrader: Email Account Status (aee25d69)
* fetcher: Errors Handling & Logging (384aad13, 22edf2b4, c92006e6)
* fetcher: Mail Fetch Order (f1639d41, 24338c2b)
* SMTP: Dont send 'QUIT' on __destruct() (dbeae227)
* issue: MTA Typo (03eeb8e2)
* sendmail: Strip "To" and "Subject" headers (ac3855a4)
* Mail Parse Error Handling (320981df, 75c5cfe9, a4e36d39, ba2d31cd, 30ad9cd4, 678098d8, 46b7899b, f85f9036)
* Issue/session revisited (49d91b05, 4fc5f5cc)
osTicket v1.17
==============
### Enhancements
@@ -50,6 +279,78 @@ osTicket v1.17-rc1
### Enhancements
* Oauth2 / Modern Email Authentication Support (d99ff6e1-163c28b5)
osTicket v1.16.6
================
### Improvements
* session: Collab View Ticket (6afe0efa)
* issue: Static forTask() (1079011f)
* issue: Featured FAQs (8df09581)
* print: Hidden Thread Events (f63fc67d)
* issue: Does Not Have A Value (d47fc2b1)
* issue: Disabled Queues Run Counts (3ff3ce93)
* issue: Queue Counts Incorrect (2428cc58)
* jquery: Update jQueryUI 1.13.2 (b896f70d)
* issue: Department Export (5418cf20)
* jquery: Update To 3.6.3 (8d39a4cc)
* i18n: make-pot (74274b5c)
* update: README PHP Version (6087bc55)
* issue: mPDF Font Files (427a8aa6)
* issue: FileUpload False (8dd2ab61)
* Add translation for "last resort" standard column labels (cb528044)
* add translation for task status (46c005a0)
* issue: getRealPath() Stub (32a229aa)
### Security
* security: Latest Security Patches 03/2023 (86f9693d, 091ddba9, ec604393, daee20fd, 9fb01bc1, 619ce0f6, 343a2b47, 8a55c29f, e2c8f799, ee7ce3d9, 263f71a3)
osTicket v1.16.5
================
### Improvements
* Fix trusted proxy wildcard check (04c0bb3a)
* issue: Html Typo (50dfe931)
* mailparse: Parse filenames with complex format (c8067212)
* validator: Email Domain Verification (3c4a20a8)
* user: Make sure User has a valid Email on create (55fbfcb5)
osTicket v1.16.4
================
### Enhancements
* i18n: Default Ticket Queue (7a5dd0eb)
* mailparse: thread_entry_recipients to (a82af698)
* issue: Unique Variable Names (0dc5a02e)
* php: 8.1 Support (22b599e3, c24ee575, dd2b9a43)
* issue: Session Cookie Refresh (1630459a)
### Improvements
* oops: jQuery UI Images (822a05fe)
* issue: Manually Sorted Topics (a80db8a7)
* issue: Queue Export Headings (47449758)
* installer: Checking Existing Install (a3f2e246)
* Catch storage error when mailing ticket response (ca5932ac)
* issue: Staff Ticket Open Session (4f3d3cff)
* issue: External Auth Regenerate Session (83252260)
* issue: Searching Numbers (e00003f6)
* Replace hacky way with ReflectionClass (55f61390)
* Issue: Installer YAML (6780f29e)
* issue: Fix Upgrader Session Issues (311089a6)
* issue: Undefined Variable ORM (4009881a)
* v1.16: Misc Bug Fixes (4c0341e2)
* issue: User/Agent Password Reset (d3de1599)
* issue: imagesx Boolean (5d06ee46)
* file: Possible Key Collision (21c71c6e)
* cli: User Import (4c43e46f)
* issue: Miscellaneous Fixes (50bc1a21, 5002bef9, e036fd9a, 35684189, 9c280b49, c2a15b64, 5e970061)
* issue: urldecode Dispatcher (c00b8e87)
* issue: Information and SectionBreak Fields On Edit (7b60c576)
* issue: Favicons Login Headers (61655ec6)
* issue: Schedules stops_on (e2dcbb69)
* Issue: Agent Permission to Assign Tickets (b4732b05)
* issue: File Upload Size (45f554ad)
### Security
* security: Username XSS (5213ff13)
* security: Refresh URL (3702a4fb)
osTicket v1.16.3
================
### Improvements

View File

@@ -125,6 +125,8 @@ elseif ($_POST) {
}
break;
}
// Rotate CSRF on successful POST
$ost->getCSRF()->rotate();
}
if ($errors && $user && $user != $thisclient)

View File

@@ -15,14 +15,20 @@
**********************************************************************/
file_exists('../main.inc.php') or die('System Error');
/*
* Why API_SESSION ??
* It indicates that the session is API - which session handler should handle as
* stateless for new sessions.
* Existing session continue to work as expected - this it's important for
* SSO authentication, which uses /api/auth/* endpoints. Such calls are not
* stateless.
*
*/
define('API_SESSION', true);
// APICALL const.
define('APICALL', true);
// Disable sessions for the API. API should be considered stateless and
// shouldn't chew up database records to store sessions
if (!defined('DISABLE_SESSION'))
define('DISABLE_SESSION', true);
require_once('../main.inc.php');
require_once(INCLUDE_DIR.'class.http.php');
require_once(INCLUDE_DIR.'class.api.php');

View File

@@ -13,15 +13,9 @@
vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
// Use sessions — it's important for SSO authentication, which uses
// /api/auth/ext
define('DISABLE_SESSION', false);
require 'api.inc.php';
# Include the main api urls
require_once INCLUDE_DIR."class.dispatcher.php";
$dispatcher = patterns('',
url_post("^/tickets\.(?P<format>xml|json|email)$", array('api.tickets.php:TicketApiController','create')),
url('^/tasks/', patterns('',
@@ -29,8 +23,8 @@ $dispatcher = patterns('',
))
);
// Send api signal so backend can register endpoints
Signal::send('api', $dispatcher);
# Call the respective function
print $dispatcher->resolve(Osticket::get_path_info());
?>

View File

@@ -18,10 +18,10 @@ ini_set('memory_limit', '256M'); //The concern here is having enough mem for ema
@chdir(dirname(__FILE__).'/'); //Change dir.
require('api.inc.php');
//Only local piping supported via pipe.php
// Only local piping supported via pipe.php
if (!osTicket::is_cli())
die(__('pipe.php only supports local piping - use http -> api/tickets.email'));
require_once(INCLUDE_DIR.'api.tickets.php');
PipeApiController::process();
PipeApiController::process('cli');
?>

View File

@@ -22,8 +22,6 @@ class Bootstrap {
#Error reporting...Good idea to ENABLE error reporting to a file. i.e display_errors should be set to false
$error_reporting = E_ALL & ~E_NOTICE & ~E_WARNING;
if (defined('E_STRICT')) # 5.4.0
$error_reporting &= ~E_STRICT;
if (defined('E_DEPRECATED')) # 5.3.0
$error_reporting &= ~(E_DEPRECATED | E_USER_DEPRECATED);
error_reporting($error_reporting); //Respect whatever is set in php.ini (sysadmin knows better??)
@@ -45,6 +43,24 @@ class Bootstrap {
}
date_default_timezone_set('UTC');
if (!function_exists('exif_imagetype')) {
function exif_imagetype ($filename) {
if ((list($width,$height,$type,) = getimagesize($filename)) !== false)
return $type;
return false;
}
}
if (!function_exists('exif_imagetype')) {
function exif_imagetype ($filename) {
if ((list($width,$height,$type,) = getimagesize($filename)) !== false)
return $type;
return false;
}
}
if (!isset($_SERVER['REMOTE_ADDR']))
$_SERVER['REMOTE_ADDR'] = '';
}
@@ -193,13 +209,22 @@ class Bootstrap {
'key' => DBSSLKEY
);
if (!db_connect(DBHOST, DBUSER, DBPASS, $options)) {
$ferror=sprintf('Unable to connect to the database — %s',db_connect_error());
}elseif(!db_select_database(DBNAME)) {
$ferror=sprintf('Unknown or invalid database: %s',DBNAME);
$hosts = explode(',', DBHOST);
foreach ($hosts as $host) {
$ferror = null;
try {
if (!db_connect($host, DBUSER, DBPASS, $options))
$ferror = sprintf('Unable to connect to the database — %s', db_connect_error());
elseif (!db_select_database(DBNAME))
$ferror = sprintf('Unknown or invalid database: %s', DBNAME);
} catch (mysqli_sql_exception $e) {
$ferror = sprintf('Database error — %s', $e->getMessage());
}
// break if no error
if (!$ferror) break;
}
if($ferror) //Fatal error
if ($ferror) //Fatal error
self::croak($ferror);
}
@@ -314,8 +339,8 @@ class Bootstrap {
static function croak($message) {
$msg = $message."\n\n".THISPAGE;
Mailer::sendmail(ADMIN_EMAIL, 'osTicket Fatal Error', $msg,
sprintf('"osTicket Alerts"<%s>', ADMIN_EMAIL));
osTicket\Mail\Mailer::sendmail(ADMIN_EMAIL, 'osTicket Fatal Error', $msg,
sprintf('"osTicket Alerts" <%s>', ADMIN_EMAIL));
//Display generic error to the user
Http::response(500, "<b>Fatal Error:</b> Contact system administrator.");
}
@@ -342,7 +367,7 @@ define('CLI_DIR', INCLUDE_DIR.'cli/');
#Current version && schema signature (Changes from version to version)
define('GIT_VERSION','$git');
define('MAJOR_VERSION', '1.17');
define('MAJOR_VERSION', '1.18');
define('THIS_VERSION', MAJOR_VERSION.'-git'); //Shown on admin panel
//Path separator
if(!defined('PATH_SEPARATOR')){
@@ -368,7 +393,7 @@ if (!defined('ROOT_PATH') && ($rp = osTicket::get_root_path(dirname(__file__))))
Bootstrap::init();
#CURRENT EXECUTING SCRIPT.
define('THISPAGE', Misc::currentURL());
define('THISPAGE', Http::url());
define('DEFAULT_MAX_FILE_UPLOADS', ini_get('max_file_uploads') ?: 5);
define('DEFAULT_PRIORITY_ID', 1);

View File

@@ -67,8 +67,9 @@ if($thisclient && $thisclient->getId() && $thisclient->isValid()){
}
/******* CSRF Protectin *************/
// Enforce CSRF protection for POSTS
if ($_POST && !$ost->checkCSRFToken()) {
// Enforce CSRF protection for state-changing methods
if (in_array($_SERVER['REQUEST_METHOD'], ['POST', 'PUT', 'PATCH', 'DELETE'])
&& !$ost->checkCSRFToken()) {
Http::redirect('index.php');
//just incase redirect fails
die('Action denied (400)!');
@@ -79,6 +80,8 @@ $ost->addExtraHeader('<meta name="csrf_token" content="'.$ost->getCSRFToken().'"
/* Client specific defaults */
define('PAGE_LIMIT', DEFAULT_PAGE_LIMIT);
define('SESSION_MAXLIFE', $thisclient ? $thisclient->getMaxIdleTime() :
SESSION_TTL);
require(INCLUDE_DIR.'class.nav.php');
$nav = new UserNav($thisclient, 'home');

View File

@@ -30,7 +30,7 @@
.flag.flag-bi {background-position: -112px -11px}
.flag.flag-bj {background-position: -128px -11px}
.flag.flag-bm {background-position: -144px -11px}
.flag.flag-bn {background-position: -160px -11px}
.flag.flag-bn {background-position: -32px -11px}
.flag.flag-bo {background-position: -176px -11px}
.flag.flag-br {background-position: -192px -11px}
.flag.flag-bs {background-position: 0px -11px}
@@ -124,6 +124,7 @@
.flag.flag-ki {background-position: -80px -77px}
.flag.flag-km {background-position: -64px -77px}
.flag.flag-kn {background-position: -112px -77px}
.flag.flag-ko {background-position: -144px -77px}
.flag.flag-kp {background-position: -128px -77px}
.flag.flag-kr {background-position: -144px -77px}
.flag.flag-kurdistan {background-position: -160px -77px}
@@ -186,7 +187,7 @@
.flag.flag-pm {background-position: -48px -121px}
.flag.flag-pn {background-position: -64px -121px}
.flag.flag-pr {background-position: -80px -121px}
.flag.flag-ps {background-position: -96px -121px}
.flag.flag-ps {background-position: -48px 0px}
.flag.flag-pt {background-position: -112px -121px}
.flag.flag-pw {background-position: -128px -121px}
.flag.flag-py {background-position: -144px -121px}
@@ -211,6 +212,7 @@
.flag.flag-sn {background-position: -192px -132px}
.flag.flag-so {background-position: -208px -132px}
.flag.flag-somaliland {background-position: -224px -132px}
.flag.flag-sq {background-position: -96px 0px}
.flag.flag-sr {background-position: -240px -132px}
.flag.flag-ss {background-position: 0 -143px}
.flag.flag-st {background-position: -16px -143px}
@@ -244,7 +246,7 @@
.flag.flag-vc {background-position: -208px -154px}
.flag.flag-ve {background-position: -224px -154px}
.flag.flag-vg {background-position: -240px -154px}
.flag.flag-vi {background-position: 0 -165px}
.flag.flag-vi {background-position: -16px -165px}
.flag.flag-vn {background-position: -16px -165px}
.flag.flag-vu {background-position: -32px -165px}
.flag.flag-wales {background-position: -48px -165px}
@@ -256,3 +258,5 @@
.flag.flag-zanzibar {background-position: -144px -165px}
.flag.flag-zm {background-position: -160px -165px}
.flag.flag-zw {background-position: -176px -165px}
.flag.flag-fil {background-position: 0px -121px}
.flag.flag-kk {background-position: -208px -77px}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 418 B

After

Width:  |  Height:  |  Size: 528 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 312 B

After

Width:  |  Height:  |  Size: 436 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

After

Width:  |  Height:  |  Size: 376 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 348 B

After

Width:  |  Height:  |  Size: 458 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 207 B

After

Width:  |  Height:  |  Size: 317 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.7 KiB

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 278 B

After

Width:  |  Height:  |  Size: 394 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 328 B

After

Width:  |  Height:  |  Size: 438 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.8 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

After

Width:  |  Height:  |  Size: 4.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 6.3 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

File diff suppressed because one or more lines are too long

View File

@@ -45,9 +45,8 @@ if ($cfg->isAuthRequiredForFiles()
) {
// Try and determine if an agent is viewing the page / file
if (strpos($_SERVER['HTTP_REFERRER'], ROOT_PATH . 'scp/') !== false) {
$_SESSION['_staff']['auth']['dest'] =
'/' . ltrim($_SERVER['REQUEST_URI'], '/');
if (strpos($_SERVER['HTTP_REFERER'], ROOT_PATH . 'scp/') !== false) {
$_SESSION['_staff']['auth']['dest'] = Http::refresh_url();
Http::redirect(ROOT_PATH.'scp/login.php');
} else {
require 'secure.inc.php';
@@ -59,7 +58,7 @@ if ($cfg->isAuthRequiredForFiles()
// and the user has access to the parent ticket!!
if ($file->verifySignature($_GET['signature'], $_GET['expires'])) {
try {
if (($s = @$_GET['s']) && strpos($file->getType(), 'image/') === 0)
if (($s = @$_GET['s']) && $file->isInlineSafeImage())
return $file->display($s);
// Download the file..
@@ -68,7 +67,8 @@ if ($file->verifySignature($_GET['signature'], $_GET['expires'])) {
$file->download($filename, $disposition, @$_GET['expires']);
}
catch (Exception $ex) {
Http::response(500, 'Unable to find that file: '.$ex->getMessage());
$ost->logError(__('File download error'), $ex->getMessage());
Http::response(500, __('Unable to retrieve the requested file'));
}
}
// else

View File

@@ -19,7 +19,12 @@ require_once INCLUDE_DIR.'class.ajax.php';
class ContentAjaxAPI extends AjaxController {
function log($id) {
function syslog($id) {
global $thisstaff;
// Ensure there is a Staff and that they are an Administrator
if (!$thisstaff || !$thisstaff->isAdmin())
Http::response(403, 'Access Denied');
if($id && ($log=Log::lookup($id))) {
$content=sprintf('<div
@@ -142,8 +147,8 @@ class ContentAjaxAPI extends AjaxController {
function manageContent($id, $lang=false) {
global $thisstaff, $cfg;
if (!$thisstaff)
Http::response(403, 'Login Required');
if (!$thisstaff || !$thisstaff->isAdmin())
Http::response(403, 'Access Denied');
$content = Page::lookup($id, $lang);
@@ -168,8 +173,8 @@ class ContentAjaxAPI extends AjaxController {
function manageNamedContent($type, $lang=false) {
global $thisstaff, $cfg;
if (!$thisstaff)
Http::response(403, 'Login Required');
if (!$thisstaff || !$thisstaff->isAdmin())
Http::response(403, 'Access Denied');
$langs = $cfg->getSecondaryLanguages();
@@ -180,10 +185,10 @@ class ContentAjaxAPI extends AjaxController {
}
function updateContent($id) {
global $thisstaff;
global $thisstaff, $cfg;
if (!$thisstaff)
Http::response(403, 'Login Required');
if (!$thisstaff || !$thisstaff->isAdmin())
Http::response(403, 'Access Denied');
elseif (!($content = Page::lookup($id)))
Http::response(404, 'No such content');
@@ -201,6 +206,7 @@ class ContentAjaxAPI extends AjaxController {
}
if (!$errors['err'])
$errors['err'] = __('Correct any errors below and try again.');
$langs = Internationalization::getConfiguredSystemLanguages();
$info = $_POST;
$errors = Format::htmlchars($errors);
include STAFFINC_DIR . 'templates/content-manage.tmpl.php';

View File

@@ -3,6 +3,7 @@
if(!defined('INCLUDE_DIR')) die('!');
require_once(INCLUDE_DIR.'class.draft.php');
require_once(INCLUDE_DIR.'class.file.php');
class DraftAjaxAPI extends AjaxController {
@@ -53,78 +54,80 @@ class DraftAjaxAPI extends AjaxController {
static function _uploadInlineImage($draft) {
global $cfg;
if (!isset($_POST['data']) && !isset($_FILES['file']))
if (!isset($_FILES['file']))
Http::response(422, "File not included properly");
# Fixup for expected multiple attachments
if (isset($_FILES['file'])) {
$file = AttachmentFile::format($_FILES['file']);
$file = AttachmentFile::format($_FILES['file']);
# Allow for data-uri uploaded files
$fp = fopen($file[0]['tmp_name'], 'rb');
if (fread($fp, 5) == 'data:') {
$data = 'data:';
while ($block = fread($fp, 8192))
$data .= $block;
$file[0] = Format::parseRfc2397($data);
list(,$ext) = explode('/', $file[0]['type'], 2);
$file[0] += array(
'name' => Misc::randCode(8).'.'.$ext,
'size' => strlen($file[0]['data']),
);
}
fclose($fp);
# TODO: Detect unacceptable attachment extension
# TODO: Verify content-type and check file-content to ensure image
$type = $file[0]['type'];
if (strpos($file[0]['type'], 'image/') !== 0)
return Http::response(403,
JsonDataEncoder::encode(array(
'error' => 'File type is not allowed',
))
);
# TODO: Verify file size is acceptable
if ($file[0]['size'] > $cfg->getMaxFileSize())
return Http::response(403,
JsonDataEncoder::encode(array(
'error' => 'File is too large',
))
);
// Paste uploads in Chrome will have a name of 'blob'
if ($file[0]['name'] == 'blob')
$file[0]['name'] = 'screenshot-'.Misc::randCode(4);
$ids = $draft->attachments->upload($file);
if (!$ids) {
if ($file[0]['error']) {
return Http::response(403,
JsonDataEncoder::encode(array(
'error' => $file[0]['error'],
))
);
}
else
return Http::response(500, 'Unable to attach image');
}
$id = (is_array($ids)) ? $ids[0] : $ids;
}
else {
$type = explode('/', $_POST['contentType']);
$info = array(
'data' => base64_decode($_POST['data']),
'name' => Misc::randCode(10).'.'.$type[1],
// TODO: Ensure _POST['contentType']
'type' => $_POST['contentType'],
// Allow one file at a time.
if (count($file) > 1)
return Http::response(403,
JsonDataEncoder::encode(array(
'error' => 'Send one file at a time',
))
);
# Allow for data-uri uploaded files
$fp = fopen($file[0]['tmp_name'], 'rb');
if (fread($fp, 5) == 'data:') {
$data = 'data:';
while ($block = fread($fp, 8192))
$data .= $block;
$file[0] = Format::parseRfc2397($data);
list(,$ext) = explode('/', $file[0]['type'], 2);
$file[0] += array(
'name' => Misc::randCode(8).'.'.$ext,
'size' => strlen($file[0]['data']),
);
// TODO: Detect unacceptable filetype
// TODO: Verify content-type and check file-content to ensure image
$id = $draft->attachments->save($info);
}
fclose($fp);
// Check file type to ensure image
$type = $file[0]['type'];
if (!AttachmentFile::isInlineSafeImageType($file[0]['type']))
return Http::response(403,
JsonDataEncoder::encode(array(
'error' => 'File type is not allowed',
))
);
// Check if file is truly an image
if (!FileUploadField::isValidFile($file[0]))
return Http::response(403,
JsonDataEncoder::encode(array(
'error' => 'File is not valid',
))
);
// Verify file size is acceptable
if ($file[0]['size'] > $cfg->getMaxFileSize())
return Http::response(403,
JsonDataEncoder::encode(array(
'error' => 'File is too large',
))
);
// Paste uploads in Chrome will have a name of 'blob'
if ($file[0]['name'] == 'blob')
$file[0]['name'] = 'screenshot-'.Misc::randCode(4);
$ids = $draft->attachments->upload($file);
if (!$ids) {
if ($file[0]['error']) {
return Http::response(403,
JsonDataEncoder::encode(array(
'error' => $file[0]['error'],
))
);
}
else
return Http::response(500, 'Unable to attach image');
}
$id = (is_array($ids)) ? $ids[0] : $ids;
if (!($f = AttachmentFile::lookup($id)))
return Http::response(500, 'Unable to attach image');
@@ -240,6 +243,7 @@ class DraftAjaxAPI extends AjaxController {
$draft = Draft::create(array(
'namespace' => $namespace,
'body' => ''
));
if (!$draft->save())
Http::response(500, 'Unable to create draft');
@@ -310,7 +314,8 @@ class DraftAjaxAPI extends AjaxController {
Http::response(403, "Login required for image upload");
$draft = Draft::create(array(
'namespace' => $namespace
'namespace' => $namespace,
'body' => ''
));
if (!$draft->save())
Http::response(500, 'Unable to create draft');
@@ -363,6 +368,9 @@ class DraftAjaxAPI extends AjaxController {
$files = array();
foreach ($images as $f) {
if (!$f->isInlineSafeImage())
continue;
$url = $f->getDownloadUrl();
$files[] = array(
// Don't send special sizing for thread items 'cause they

View File

@@ -25,7 +25,6 @@ class EmailAjaxAPI extends AjaxController {
$info = $errors = [];
if ($_POST && $account->saveAuth($auth, $form, $errors)) {
if ($account->isOAuthAuth()
&& $account->isEnabled()
&& $account->shouldAuthorize()) {
Http::response(201, JsonDataEncoder::encode([
'redirect' => sprintf('emails.php?id=%d&do=autho&bk=%s',
@@ -40,4 +39,21 @@ class EmailAjaxAPI extends AjaxController {
$template = sprintf('email-%sauth.tmpl.php', $authtype);
include INCLUDE_DIR . "staff/templates/$template";
}
/*
* Delete the OAuth2 token
*/
function deleteToken($id, $type) {
// Check to make sure the email exists
if (!($email=Email::lookup($id)))
Http::response(404, 'Unknown Email');
// Get the authentication account
if (!($account=$email->getAuthAccount($type)))
Http::response(404, 'Unknown Authentication Type');
// Destory the account config which will delete the token
if ($account->destroyConfig())
Http::response(201, __('Token Deleted Successfully.'));
// Couldn't delete the Token
Http::response(404, 'Unable to delete token.');
}
}

View File

@@ -13,6 +13,9 @@ class NoteAjaxAPI extends AjaxController {
Http::response(403, "Login required");
elseif (!($note = QuickNote::lookup($id)))
Http::response(205, "Note not found");
elseif (($note->staff_id !== $thisstaff->getId())
&& !$thisstaff->isAdmin())
Http::response(403, "Access Denied");
Http::response(200, $note->display());
}
@@ -24,6 +27,9 @@ class NoteAjaxAPI extends AjaxController {
Http::response(403, "Login required");
elseif (!($note = QuickNote::lookup($id)))
Http::response(205, "Note not found");
elseif (($note->staff_id !== $thisstaff->getId())
&& !$thisstaff->isAdmin())
Http::response(403, "Access Denied");
elseif (!isset($_POST['note']) || !$_POST['note'])
Http::response(422, "Send `note` parameter");
@@ -41,6 +47,9 @@ class NoteAjaxAPI extends AjaxController {
Http::response(403, "Login required");
elseif (!($note = QuickNote::lookup($id)))
Http::response(205, "Note not found");
elseif (($note->staff_id !== $thisstaff->getId())
&& !$thisstaff->isAdmin())
Http::response(403, "Access Denied");
elseif (!$note->delete())
Http::response(500, "Unable to remove note");
@@ -52,6 +61,9 @@ class NoteAjaxAPI extends AjaxController {
if (!$thisstaff)
Http::response(403, "Login required");
elseif (!in_array(substr($ext_id, 0, 1), ['O', 'U'])
&& !$thisstaff->isAdmin())
Http::response(403, "Access Denied");
elseif (!isset($_POST['note']) || !$_POST['note'])
Http::response(422, "Send `note` parameter");

View File

@@ -30,7 +30,7 @@ class OrgsAjaxAPI extends AjaxController {
if (!$_REQUEST['q'])
return $this->json_encode(array());
$q = $_REQUEST['q'];
$q = Format::sanitize($_REQUEST['q']);
$limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
if (strlen(Format::searchable($q)) < 3)
@@ -55,7 +55,7 @@ class OrgsAjaxAPI extends AjaxController {
foreach ($orgs as $O) {
list($id, $name) = $O;
$matched[] = array('name' => Format::htmlchars($name), 'info' => $name,
'id' => $id, '/bin/true' => $_REQUEST['q']);
'id' => $id, '/bin/true' => Format::htmlchars($q));
}
return $this->json_encode(array_values($matched));

View File

@@ -31,7 +31,7 @@ class SearchAjaxAPI extends AjaxController {
$search = new AdhocSearch(array(
'root' => 'T',
'staff_id' => $thisstaff->getId(),
'parent_id' => @$_GET['parent_id'] ?: 0,
'parent_id' => (int) @$_GET['parent_id'] ?: 0,
));
if ($search->parent_id) {
$search->flags |= SavedSearch::FLAG_INHERIT_COLUMNS;
@@ -168,7 +168,7 @@ class SearchAjaxAPI extends AjaxController {
'title' => __('Add Queue'),
'root' => 'T',
'staff_id' => $thisstaff->getId(),
'parent_id' => $_GET['pid'],
'parent_id' => (int) $_GET['pid'],
));
$this->_tryAgain($search);
}
@@ -188,11 +188,16 @@ class SearchAjaxAPI extends AjaxController {
'root' => 'T',
'staff_id' => $thisstaff->getId()
));
$new = true;
}
if (false === $this->_saveSearch($search))
return;
//store saved search count when it is created
if ($new)
$search->counts($thisstaff, false);
$info = array(
'msg' => sprintf('%s %s %s',
__('Search'),
@@ -346,17 +351,16 @@ class SearchAjaxAPI extends AjaxController {
function addCondition() {
global $thisstaff;
if (!$thisstaff) {
if (!$thisstaff)
Http::response(403, 'Agent login is required');
}
elseif (!isset($_GET['field']) || !isset($_GET['id'])
|| !isset($_GET['object_id'])
) {
|| !isset($_GET['object_id']))
Http::response(400, '`field`, `id`, and `object_id` parameters required');
}
elseif (!is_numeric($_GET['object_id'])) {
elseif (!is_numeric($_GET['object_id']))
Http::response(400, '`object_id` should be an integer');
}
elseif (!is_numeric($_GET['id']))
Http::response(400, '`id` should be an integer');
$fields = SavedSearch::getSearchableFields('Ticket');
if (!isset($fields[$_GET['field']])) {
Http::response(400, sprintf('%s: No such searchable field'),
@@ -382,8 +386,8 @@ class SearchAjaxAPI extends AjaxController {
Http::response(400, '`prop` and `condition` parameters required');
}
$prop = $_GET['prop'];
$id = $_GET['condition'];
$prop = Format::htmlchars($_GET['prop']);
$id = Format::htmlchars($_GET['condition']);
include STAFFINC_DIR . 'templates/queue-column-condition-prop.tmpl.php';
}

View File

@@ -59,10 +59,17 @@ class TasksAjaxAPI extends AjaxController {
}
function triggerThreadAction($task_id, $thread_id, $action) {
global $thisstaff;
if (!($task = Task::lookup($task_id))
|| !$task->checkStaffPerm($thisstaff))
Http::response(404, 'No such task thread entry');
$thread = ThreadEntry::lookup($thread_id);
if (!$thread)
Http::response(404, 'No such task thread entry');
if ($thread->getThread()->getObjectId() != $task_id)
if ($thread->getThread()->getObjectType() != 'A'
|| $thread->getThread()->getObjectId() != $task_id)
Http::response(404, 'No such task thread entry');
$valid = false;

View File

@@ -23,7 +23,7 @@ include_once INCLUDE_DIR . 'class.thread_actions.php';
class TicketsAjaxAPI extends AjaxController {
function lookup() {
function lookup($wildcard=false) {
global $thisstaff;
$limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
@@ -53,7 +53,7 @@ class TicketsAjaxAPI extends AjaxController {
return $this->encode(array());
global $ost;
$hits = $ost->searcher->find($q, $hits, false);
$hits = $ost->searcher->find($q, $hits, false, ['boolean' => $wildcard]);
$T = array();
if (preg_match('/\d{2,}[^*]/', $q, $T)) {
@@ -62,10 +62,10 @@ class TicketsAjaxAPI extends AjaxController {
elseif (preg_match('/(.*@.{2,})|(.{2,}@.*)/', $q, $T)) {
$hits = $this->lookupByEmail($limit, $visibility, $hits);
}
elseif (!count($hits) && preg_match('`\w$`u', $q)) {
elseif (!$wildcard && !count($hits) && preg_match('`^[a-zA-Z0-9]+$`u', $q)) {
// Do wild-card fulltext search
$_REQUEST['q'] = $q.'*';
return $this->lookup();
return $this->lookup(true);
}
// TODO: Why not aggregate based on relationship?
@@ -777,6 +777,8 @@ class TicketsAjaxAPI extends AjaxController {
if (!($ticket=Ticket::lookup($tid)))
Http::response(404, __('No such ticket'));
elseif (!$ticket->checkStaffPerm($thisstaff))
Http::response(403, __('Permission denied'));
elseif (!($field=$ticket->getField($fid)))
Http::response(404, __('No such field'));
@@ -1134,6 +1136,7 @@ class TicketsAjaxAPI extends AjaxController {
$depts = $tickets->values_flat('dept_id');
}
//agents in depts $thisstaff can access
$members = $thisstaff->getDeptAgents(array('available' => true));
if ($depts) {
@@ -1211,6 +1214,9 @@ class TicketsAjaxAPI extends AjaxController {
$f->configure('prompt', $prompt);
if ($_POST && $form->isValid()) {
// we need to clear this relation so we can create a fresh
// query that will get roles for all depts this agent can access
unset($thisstaff->ht['dept_access']);
foreach ($_POST['tids'] as $tid) {
if (($t=Ticket::lookup($tid))
// Make sure the agent is allowed to
@@ -1698,10 +1704,17 @@ class TicketsAjaxAPI extends AjaxController {
}
function triggerThreadAction($ticket_id, $thread_id, $action) {
global $thisstaff;
if (!($ticket = Ticket::lookup($ticket_id))
|| !$ticket->checkStaffPerm($thisstaff))
Http::response(404, 'No such ticket thread entry');
$thread = ThreadEntry::lookup($thread_id);
if (!$thread)
Http::response(404, 'No such ticket thread entry');
if ($thread->getThread()->getObjectId() != $ticket_id)
if ($thread->getThread()->getObjectType() != 'T'
|| $thread->getThread()->getObjectId() != $ticket_id)
Http::response(404, 'No such ticket thread entry');
$valid = false;
@@ -2031,7 +2044,7 @@ class TicketsAjaxAPI extends AjaxController {
} else {
$filename = sprintf('%s Tickets-%s.csv',
$queue->getName(),
strftime('%Y%m%d'));
date('Ymd'));
}
try {

View File

@@ -34,7 +34,7 @@ class UsersAjaxAPI extends AjaxController {
if (!$_REQUEST['q'])
return $this->json_encode($matches);
$q = $_REQUEST['q'];
$q = Format::sanitize($_REQUEST['q']);
$limit = isset($_REQUEST['limit']) ? (int) $_REQUEST['limit']:25;
$users=array();
$emails=array();
@@ -107,7 +107,7 @@ class UsersAjaxAPI extends AjaxController {
}
$name = Format::htmlchars(new UsersName($name));
$matches[] = array('email'=>$email, 'name'=>$name, 'info'=>"$email - $name",
"id" => $id, "/bin/true" => $_REQUEST['q']);
"id" => $id, "/bin/true" => $q);
}
usort($matches, function($a, $b) { return strcmp($a['name'], $b['name']); });
}
@@ -121,7 +121,12 @@ class UsersAjaxAPI extends AjaxController {
if(!$thisstaff)
Http::response(403, 'Login Required');
elseif(!($user = User::lookup($id)))
if (!$thisstaff->hasPerm(User::PERM_DIRECTORY)
&& !$thisstaff->hasPerm(User::PERM_EDIT))
Http::response(403, 'Permission Denied');
if(!($user = User::lookup($id)))
Http::response(404, 'Unknown user');
$info = array(
@@ -275,6 +280,14 @@ class UsersAjaxAPI extends AjaxController {
}
function getUser($id=false) {
global $thisstaff;
if (!$thisstaff)
Http::response(403, 'Login Required');
if (!$thisstaff->hasPerm(User::PERM_DIRECTORY)
&& !$thisstaff->hasPerm(User::PERM_EDIT))
Http::response(403, 'Permission Denied');
if(($user=User::lookup(($id) ? $id : $_REQUEST['id'])))
Http::response(201, $user->to_json(), 'application/json');
@@ -323,7 +336,7 @@ class UsersAjaxAPI extends AjaxController {
elseif (!$bk || !$id)
Http::response(422, 'Backend and user id required');
elseif (!($backend = AuthenticationBackend::getSearchDirectoryBackend($bk))
|| !($user_info = $backend->lookup($id)))
|| !($user_info = $backend->lookup(html_entity_decode($id))))
Http::response(404, 'User not found');
$form = UserForm::getUserForm()->getForm($user_info);
@@ -422,6 +435,8 @@ class UsersAjaxAPI extends AjaxController {
Http::response(403, 'Login Required');
elseif (!($user = User::lookup($id)))
Http::response(404, 'Unknown user');
elseif (!$thisstaff->hasPerm(User::PERM_EDIT))
Http::response(403, 'Access Denied');
$info = array();
$info['title'] = sprintf(__('Organization for %s'),
@@ -434,6 +449,9 @@ class UsersAjaxAPI extends AjaxController {
if (!($org = Organization::lookup($_POST['orgid'])))
$info['error'] = __('Unknown organization selected');
} else { //Creating new org.
if (!$thisstaff->hasPerm(Organization::PERM_CREATE))
Http::response(403, 'Permission Denied');
$form = OrganizationForm::getDefaultForm()->getForm($_POST);
if (!($org = Organization::fromForm($form)))
$info['error'] = __('Unable to create organization.')

View File

@@ -3,40 +3,45 @@
include_once INCLUDE_DIR.'class.cron.php';
class CronApiController extends ApiController {
function execute() {
if(!($key=$this->requireApiKey()) || !$key->canExecuteCron())
if (!($key=$this->requireApiKey()) || !$key->canExecuteCron())
return $this->exerr(401, __('API key not authorized'));
$this->run();
}
/* private */
function run() {
global $ost;
protected function run() {
Cron::run();
$ost->logDebug(__('Cron Job'),__('Cron job executed').' ['.$_SERVER['REMOTE_ADDR'].']');
// TODO: Add elapsed time to the debug log
$this->debug(__('Cron Job'),
sprintf('%s [%s]', __('Cron job executed'), $this->getRemoteAddr()));
$this->response(200,'Completed');
}
}
class LocalCronApiController extends CronApiController {
function response($code, $resp) {
public function isCli() {
return true;
}
if($code == 200) //Success - exit silently.
protected function getRemoteAddr() {
// Local Cron doesn't have IP Addr set
return 'CLI';
}
public function response($code, $response) {
if ($code == 200) //Success - exit silently.
exit(0);
//On error echo the response (error)
echo $resp;
echo $response;
exit(1);
}
static function call() {
$cron = new LocalCronApiController();
$cron = new LocalCronApiController('cli');
$cron->run();
}
}

View File

@@ -43,15 +43,22 @@ class TicketApiController extends ApiController {
foreach ($form->getFields() as $field)
$supported[] = $field->get('name');
if(!strcasecmp($format, 'email')) {
$supported = array_merge($supported, array('header', 'mid',
'emailId', 'to-email-id', 'ticketId', 'reply-to', 'reply-to-name',
'in-reply-to', 'references', 'thread-type', 'system_emails',
'mailflags' => array('bounce', 'auto-reply', 'spam', 'viral'),
'recipients' => array('*' => array('name', 'email', 'source'))
));
$supported['attachments']['*'][] = 'cid';
switch ($format) {
case 'email':
$supported = array_merge($supported, [
'header', 'mid', 'emailId', 'to-email-id', 'ticketId', 'reply-to',
'reply-to-name', 'in-reply-to', 'references', 'thread-type', 'system_emails',
'mailflags' => ['bounce', 'auto-reply', 'spam', 'viral'],
'recipients' => ['*' => ['name', 'email', 'source']]
]);
$supported['attachments']['*'][] = 'cid';
break;
case 'json':
case 'xml':
$supported = array_merge($supported, [
'duedate', 'slaId', 'staffId'
]);
break;
}
return $supported;
@@ -78,7 +85,7 @@ class TicketApiController extends ApiController {
$data['attachments'] = array();
//Validate attachments: Do error checking... soft fail - set the error and pass on the request.
if ($data['attachments'] && is_array($data['attachments'])) {
if (isset($data['attachments']) && is_array($data['attachments'])) {
foreach($data['attachments'] as &$file) {
if ($file['encoding'] && !strcasecmp($file['encoding'], 'base64')) {
if(!($file['data'] = base64_decode($file['data'], true)))
@@ -105,66 +112,73 @@ class TicketApiController extends ApiController {
function create($format) {
if(!($key=$this->requireApiKey()) || !$key->canCreateTickets())
if (!($key=$this->requireApiKey()) || !$key->canCreateTickets())
return $this->exerr(401, __('API key not authorized'));
$ticket = null;
if(!strcasecmp($format, 'email')) {
# Handle remote piped emails - could be a reply...etc.
$ticket = $this->processEmail();
if (!strcasecmp($format, 'email')) {
// Process remotely piped emails - could be a reply...etc.
$ticket = $this->processEmailRequest();
} else {
# Parse request body
// Get and Parse request body data for the format
$ticket = $this->createTicket($this->getRequest($format));
}
if(!$ticket)
return $this->exerr(500, __("Unable to create new ticket: unknown error"));
$this->response(201, $ticket->getNumber());
if ($ticket)
$this->response(201, $ticket->getNumber());
else
$this->exerr(500, _S("unknown error"));
}
/* private helper functions */
function createTicket($data) {
function createTicket($data, $source = 'API') {
# Pull off some meta-data
$alert = (bool) (isset($data['alert']) ? $data['alert'] : true);
$autorespond = (bool) (isset($data['autorespond']) ? $data['autorespond'] : true);
# Assign default value to source if not defined, or defined as NULL
$data['source'] = isset($data['source']) ? $data['source'] : 'API';
// Assign default value to source if not defined, or defined as NULL
$data['source'] ??= $source;
# Create the ticket with the data (attempt to anyway)
// Create the ticket with the data (attempt to anyway)
$errors = array();
if (($ticket = Ticket::create($data, $errors, $data['source'],
$autorespond, $alert)) && !$errors)
return $ticket;
$ticket = Ticket::create($data, $errors, $data['source'], $autorespond, $alert);
# Return errors (?)
// Ticket create failed Bigly - got errors?
$title = null;
// Got errors?
if (count($errors)) {
if(isset($errors['errno']) && $errors['errno'] == 403) {
// If CLI then throw a TicketDenied Exception. Mail Fetcher
// will handle logging the message as needed
if (PHP_SAPI == 'cli') {
$msg = sprintf("%s: %s\n\n%s",
__('Ticket denied'),
$data['email'],
$errors['err']);
throw new TicketDenied($msg);
}
return $this->exerr(403, __('Ticket denied'));
} else
return $this->exerr(
400,
__("Unable to create new ticket: validation errors").":\n"
.Format::array_implode(": ", "\n", $errors)
);
} elseif (!$ticket) {
return $this->exerr(500, __("Unable to create new ticket: unknown error"));
// Ticket denied? Say so loudly so it can standout from generic
// validation errors
if (isset($errors['errno']) && $errors['errno'] == 403) {
$title = _S('Ticket denied');
$error = sprintf("%s: %s\n\n%s",
$title, $data['email'], $errors['err']);
} else {
// unpack the errors
$error = Format::array_implode("\n", "\n", $errors);
}
} else {
// unknown reason - default
$error = _S('unknown error');
}
return $ticket;
$error = sprintf('%s :%s',
_S('Unable to create new ticket'), $error);
return $this->exerr($errors['errno'] ?: 500, $error, $title);
}
function processEmail($data=false) {
function processEmailRequest() {
return $this->processEmail();
}
function processEmail($data=false, array $defaults = []) {
try {
if (!$data)
@@ -175,6 +189,7 @@ class TicketApiController extends ApiController {
throw new EmailParseError($ex->getMessage());
}
$data = array_merge($defaults, $data);
$seen = false;
if (($entry = ThreadEntry::lookupByEmailHeaders($data, $seen))
&& ($message = $entry->postEmail($data))
@@ -199,25 +214,33 @@ class TicketApiController extends ApiController {
// will always create new "Tickets". All other objects will need to
// be created via the web interface or the API
try {
return $this->createTicket($data);
} catch (TicketDenied $ex) {
// Log the mid so we don't process this email again!
$entry = new ThreadEntry();
$entry->logEmailHeaders(0, $data['mid']);
// rethrow the exception
throw $ex;
return $this->createTicket($data, 'Email');
} catch (TicketApiError $err) {
// Check if the ticket was denied by a filter or banlist
if ($err->isDenied() && $data['mid']) {
// We need to log the Message-Id (mid) so we don't
// process the same email again in subsequent fetches
$entry = new ThreadEntry();
$entry->logEmailHeaders(0, $data['mid']);
// throw TicketDenied exception so the caller can handle it
// accordingly
throw new TicketDenied($err->getMessage());
} else {
// otherwise rethrow this bad baby as it is!
throw $err;
}
}
}
}
//Local email piping controller - no API key required!
class PipeApiController extends TicketApiController {
//Overwrite grandparent's (ApiController) response method.
// Overwrite grandparent's (ApiController) response method.
function response($code, $resp) {
//Use postfix exit codes - instead of HTTP
// It's important to use postfix exit codes for local piping instead
// of HTTP's so the piping script can process them accordingly
switch($code) {
case 201: //Success
$exitcode = 0;
@@ -242,20 +265,30 @@ class PipeApiController extends TicketApiController {
default: //Temp (unknown) failure - retry
$exitcode = 75;
}
//echo "$code ($exitcode):$resp";
//We're simply exiting - MTA will take care of the rest based on exit code!
exit($exitcode);
}
static function process() {
$pipe = new PipeApiController();
if(($ticket=$pipe->processEmail()))
static function process($sapi=null) {
$pipe = new PipeApiController($sapi);
if (($ticket=$pipe->processEmail()))
return $pipe->response(201,
is_object($ticket) ? $ticket->getNumber() : $ticket);
return $pipe->exerr(416, __('Request failed - retry again!'));
}
static function local() {
return self::process('cli');
}
}
class TicketApiError extends Exception {
// Check if exception is because of denial
public function isDenied() {
return ($this->getCode() === 403);
}
}
class TicketDenied extends Exception {}

View File

@@ -24,14 +24,19 @@ require_once (INCLUDE_DIR.'class.api.php');
* call controller should inherit from this class in order to maintain
* consistency.
*/
class AjaxController extends Controller {
class AjaxController extends ApiController {
function staffOnly() {
global $thisstaff;
if(!$thisstaff || !$thisstaff->isValid()) {
Http::response(401,sprintf(__('Access Denied. IP %s'),$_SERVER['REMOTE_ADDR']));
if (!$thisstaff || !$thisstaff->isValid()) {
$this->exerr(401, sprintf('%s (%s)',
__('Access Denied'), $_SERVER['REMOTE_ADDR']));
}
return true;
}
/**
* Convert a PHP array into a JSON-encoded string
*/
@@ -44,6 +49,6 @@ class AjaxController extends Controller {
}
function get($var, $default=null) {
return (isset($_GET[$var])) ? $_GET[$var] : $default;
return $_GET[$var] ?: $default;
}
}

View File

@@ -146,7 +146,7 @@ class API {
$sql='INSERT INTO '.API_KEY_TABLE.' SET '.$sql
.',created=NOW() '
.',ipaddr='.db_input($vars['ipaddr'])
.',apikey='.db_input(strtoupper(md5(time().$vars['ipaddr'].md5(Misc::randCode(16)))));
.',apikey='.db_input(strtoupper(Misc::randCode(48, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789')));
if(db_query($sql) && ($id=db_insert_id()))
return $id;
@@ -169,26 +169,49 @@ class API {
class ApiController extends Controller {
var $apikey;
private $sapi;
private $key;
public function __construct($sapi = null) {
// Set API Interface the request is coming from
$this->sapi = $sapi ?: (osTicket::is_cli() ? 'cli' : 'http');
}
public function isCli() {
return (strcasecmp($this->sapi, 'cli') === 0);
}
private function getInputStream() {
return $this->isCli() ? 'php://stdin' : 'php://input';
}
protected function getRemoteAddr() {
return $_SERVER['REMOTE_ADDR'];
}
protected function getApiKey() {
return $_SERVER['HTTP_X_API_KEY'];
}
function requireApiKey() {
# Validate the API key -- required to be sent via the X-API-Key
# header
if(!($key=$this->getApiKey()))
// Require a valid API key sent as X-API-Key HTTP header
// see getApiKey method.
if (!($key=$this->getKey()))
return $this->exerr(401, __('Valid API key required'));
elseif (!$key->isActive() || $key->getIPAddr()!=$_SERVER['REMOTE_ADDR'])
elseif (!$key->isActive() || $key->getIPAddr() != $this->getRemoteAddr())
return $this->exerr(401, __('API key not found/active or source IP not authorized'));
return $key;
}
function getApiKey() {
function getKey() {
// Lookup record using sent API Key && IP Addr
if (!$this->key
&& ($key=$this->getApiKey())
&& ($ip=$this->getRemoteAddr()))
$this->key = API::lookupByKey($key, $ip);
if (!$this->apikey && isset($_SERVER['HTTP_X_API_KEY']) && isset($_SERVER['REMOTE_ADDR']))
$this->apikey = API::lookupByKey($_SERVER['HTTP_X_API_KEY'], $_SERVER['REMOTE_ADDR']);
return $this->apikey;
return $this->key;
}
/**
@@ -196,16 +219,20 @@ class ApiController extends Controller {
* hashtable. For JSON formats, this is mostly a noop, the conversion
* work will be done for XML requests
*/
function getRequest($format) {
$input = osTicket::is_cli()?'php://stdin':'php://input';
function getRequest($format, $validate=true) {
$input = $this->getInputStream();
if (!($stream = @fopen($input, 'r')))
$this->exerr(400, __("Unable to read request body"));
$this->exerr(400, sprintf('%s (%s)',
__("Unable to read request body"), $input));
return $this->parseRequest($stream, $format);
return $this->parseRequest($stream, $format, $validate);
}
function getEmailRequest() {
return $this->getRequest('email');
if (!($data=$this->getRequest('email', false)))
$this->exerr(400, __("Unable to read email request"));
return $data;
}
function parseRequest($stream, $format, $validate=true) {
@@ -213,7 +240,7 @@ class ApiController extends Controller {
switch(strtolower($format)) {
case 'xml':
if (!function_exists('xml_parser_create'))
$this->exerr(501, __('XML extension not supported'));
return $this->exerr(501, __('XML extension not supported'));
$parser = new ApiXmlDataParser();
break;
case 'json':
@@ -226,9 +253,8 @@ class ApiController extends Controller {
return $this->exerr(415, __('Unsupported data format'));
}
if (!($data = $parser->parse($stream))) {
if (!($data = $parser->parse($stream)) || !is_array($data)) {
$this->exerr(400, $parser->lastError());
throw new Exception($parser->lastError());
}
//Validate structure of the request.
@@ -265,14 +291,11 @@ class ApiController extends Controller {
} elseif (in_array($key, $structure)) {
continue;
}
if ($strict)
return $this->exerr(400, sprintf(__("%s: Unexpected data received in API request"), "$prefix$key"));
else
$ost->logWarning(__('API Unexpected Data'),
sprintf(__("%s: Unexpected data received in API request"), "$prefix$key"),
false);
}
$error = sprintf(__("%s: Unexpected data received in API request"),
"$prefix$key");
$this->onError(400, $error, __('Unexpected Data'), !$strict);
}
return true;
}
@@ -288,34 +311,66 @@ class ApiController extends Controller {
$strict);
}
protected function debug($subj, $msg) {
return $this->log($subj, $msg, LOG_DEBUG);
}
protected function logError($subj, $msg, $fatal=false) {
// If error is not fatal then log it as a warning
return $this->log($subj, $msg, $fatal ? LOG_ERR : LOG_WARN);
}
protected function log($title, $msg, $level = LOG_WARN) {
global $ost;
switch ($level) {
case LOG_WARN:
case LOG_ERR:
// We are disabling email alerts on API errors / warnings
// due to potential abuse or loops that might cause email DOS
$ost->log($level, $title, $msg, false);
break;
case LOG_DEBUG:
default:
$ost->logDebug($title, $msg);
}
return true;
}
/**
* API error & logging and response!
*
* final - don not override downstream.
*/
final public function onError($code, $error, $title = null,
$logOnly = false) {
/* If possible - DO NOT - overwrite the method downstream */
function exerr($code, $error='') {
global $ost;
if($error && is_array($error))
// Unpack the errors to string if error is an array
if ($error && is_array($error))
$error = Format::array_implode(": ", "\n", $error);
//Log the error as a warning - include api key if available.
// Log the error
$msg = $error;
if($_SERVER['HTTP_X_API_KEY'])
$msg.="\n*[".$_SERVER['HTTP_X_API_KEY']."]*\n";
$ost->logWarning(__('API Error')." ($code)", $msg, false);
// TODO: Only include API Key when in debug mode to avoid
// potentialialy leaking a valid key in system logs
if (($key=$this->getApiKey()))
$msg .= sprintf("\n[%s]\n", Format::shroud($key, 8, 16));
if (PHP_SAPI != 'cli') {
$this->response($code, $error); //Responder should exit...
}
return false;
}
$title = sprintf('%s (%s)', $title ?: __('API Error'), $code);
$this->logError($title, $msg, $logOnly);
//Default response method - can be overwritten in subclasses.
function response($code, $resp) {
Http::response($code, $resp);
exit();
// If the error is not deemed fatal then simply return
if ($logOnly)
return;
// If the API Interface is CLI then throw a TicketApiError exception
// so the caller can handle the error gracefully.
// Note that we set the error code as well
if ($this->isCli())
throw new TicketApiError($error, $code);
// Respond and exit since HTTP endpoint requests
// are considered stateless
$this->response($code, $error);
}
}
@@ -332,8 +387,8 @@ class ApiXmlDataParser extends XmlDataParser {
function fixup($current) {
global $cfg;
if($current['ticket'])
$current = $current['ticket'];
if (isset($current['ticket']))
$current = $current['ticket'];
if (!is_array($current))
return $current;
@@ -439,17 +494,17 @@ class ApiEmailDataParser extends EmailDataParser {
function fixup($data) {
global $cfg;
if(!$data) return $data;
if (!$data) return $data;
$data['source'] = 'Email';
if(!$data['subject'])
if (!$data['subject'])
$data['subject'] = '[No Subject]';
if(!$data['emailId'])
if (!$data['emailId'])
$data['emailId'] = $cfg->getDefaultEmailId();
if(!$cfg->useEmailPriority())
if( !$cfg->useEmailPriority())
unset($data['priorityId']);
return $data;

View File

@@ -120,10 +120,11 @@ extends InstrumentedList {
}
$attachments = array();
// Format $new for upload() with new name
foreach ($ids as $id=>$name) {
foreach ($ids as $id=>$value) {
if (is_array($value)) list('id' => $id, 'name' => $value) = $value;
$attachments[] = array(
'id' => $id,
'name' => $name
'name' => $value
);
}
// Everything remaining in $attachments is truly new
@@ -191,7 +192,7 @@ extends InstrumentedList {
function getInlines($lang=false) { return $this->_getList(false, true, $lang); }
function getSeparates($lang=false) { return $this->_getList(true, false, $lang); }
function getAll($lang=false) { return $this->_getList(true, true, $lang); }
function count($lang=false) { return count($this->getSeparates($lang)); }
function count($lang=false): int { return count($this->getSeparates($lang)); }
function _getList($separates=false, $inlines=false, $lang=false) {
$base = $this;

View File

@@ -419,7 +419,7 @@ abstract class AuthenticationBackend extends ServiceRegistry {
if ($bk instanceof AuthDirectorySearch)
$backends[$bk->getBkId()] = $bk;
return array_unique($backends);
return array_unique($backends, SORT_REGULAR);
}
static function searchUsers($query) {
@@ -1255,7 +1255,7 @@ class PasswordResetTokenBackend extends StaffAuthenticationBackend {
|| $id != $staff->getId())
$errors['msg'] = __('Invalid reset token');
elseif (!($ts = $_config->lastModified($_POST['token']))
&& ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
|| ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
$errors['msg'] = __('Invalid reset token');
elseif (!$staff->forcePasswdRest())
$errors['msg'] = __('Unable to reset password');
@@ -1482,7 +1482,7 @@ class ClientPasswordResetTokenBackend extends UserAuthenticationBackend {
|| $id != 'c'.$client->getId())
$errors['msg'] = __('Invalid reset token');
elseif (!($ts = $_config->lastModified($_POST['token']))
&& ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
|| ($ost->getConfig()->getPwResetWindow() < (time() - strtotime($ts))))
$errors['msg'] = __('Invalid reset token');
elseif (!$acct->forcePasswdReset())
$errors['msg'] = __('Unable to reset password');
@@ -1594,7 +1594,7 @@ abstract class PasswordPolicy extends ServiceRegistry {
return false;
}
return SessionData::objects()->filter($criteria)->delete();
return DatabaseSessionRecord::objects()->filter($criteria)->delete();
}
}
Signal::connect('auth.clean', array('PasswordPolicy', 'cleanSessions'));
@@ -1622,9 +1622,13 @@ extends PasswordPolicy {
}
function onSet($passwd, $current) {
if (strlen($passwd) < 6) {
$pwdlen = strlen($passwd);
if ($pwdlen < 6) {
throw new BadPassword(
__('Password must be at least 6 characters'));
} elseif ($pwdlen > 128) {
throw new BadPassword(
sprintf('%s - %s', __('Password is too long'), __('maximum of 128 characters allowed')));
}
// XXX: Changing case is technicall changing the password
if (0 === strcasecmp($passwd, $current)) {

View File

@@ -311,6 +311,12 @@ extends VerySimpleModel {
return true;
}
function staffCanAccess($staff) {
if (!$staff instanceof Staff)
return false;
return (!$this->dept || (($role = $staff->getRole($this->dept)) && $role->hasPerm(Canned::PERM_MANAGE)));
}
}
RolePermission::register( /* @trans */ 'Knowledgebase', Canned::getPermissions());

View File

@@ -497,7 +497,7 @@ class ClientAccount extends UserAccount {
if ($vars['backend']) {
$this->set('backend', $vars['backend']);
if ($vars['username'])
$this->set('username', $vars['username']);
$this->set('username', Format::sanitize($vars['username']));
}
if ($vars['passwd1']) {

View File

@@ -77,7 +77,12 @@ class Config {
}
function exists($key) {
return $this->get($key, null) ? true : false;
if ((isset($this->session) && array_key_exists($key, $this->session))
|| array_key_exists($key, $this->config)
|| array_key_exists($key, $this->defaults))
return true;
return false;
}
function set($key, $value) {
@@ -190,7 +195,9 @@ extends VerySimpleModel {
))->delete();
}
static function getConfigsByNamespace(?string $namespace=null, $key, $value=false) {
static function getConfigsByNamespace(?string $namespace, $key, $value=false) {
$filter = array();
$filter['key'] = $key;

View File

@@ -23,33 +23,22 @@ abstract class Controller {
return true;
}
/**
* error & logging and response!
*
*/
function exerr($code, $error='') {
global $ost;
if ($error && is_array($error))
$error = Format::array_implode(": ", "\n", $error);
//Log the error as a warning - include api key if available.
$msg = $error;
if ($_SERVER['HTTP_X_API_KEY'])
$msg.="\n*[".$_SERVER['HTTP_X_API_KEY']."]*\n";
$ost->logWarning(__('Error')." ($code)", $msg, false);
if (PHP_SAPI == 'cli') {
fwrite(STDERR, "({$code}) $error\n");
} else {
$this->response($code, $error); //Responder should exit...
}
return false;
// Wrapper for onFatalError
public function exerr($code, $error='') {
return $this->onFatalError($code, $error);
}
//Default response method - can be overwritten in subclasses.
function response($code, $resp) {
Http::response($code, $resp);
public function onFatalError($code, $error) {
$this->onError($code, $error);
// On error should exit but we're making doubly sure
$this->response($code, $error);
exit();
}
protected function response($code, $response) {
Http::response($code, $response);
exit();
}
abstract function onError($code, $error, $title=null, $logOnly=false);
}

View File

@@ -53,8 +53,7 @@ class Cron {
static function CleanExpiredSessions() {
require_once(INCLUDE_DIR.'class.ostsession.php');
$backend = new DbSessionBackend();
$backend->cleanup();
osTicketSession::cleanup();
}
static function CleanPwResets() {

View File

@@ -604,7 +604,7 @@ implements TemplateVariable, Searchable {
return true;
}
function export($dept, $criteria=null, $filename='') {
static function export($dept, $criteria=null, $filename='') {
include_once(INCLUDE_DIR.'class.error.php');
$members = $dept->getMembers();

View File

@@ -29,10 +29,14 @@ class Dispatcher {
function resolve($url, $args=null) {
if ($this->file) { $this->lazy_load(); }
# Support HTTP method emulation with the _method GET argument
if (isset($_GET['_method'])) {
$_SERVER['REQUEST_METHOD'] = strtoupper($_GET['_method']);
unset($_GET['_method']);
if ($_SERVER['REQUEST_METHOD'] == 'POST'
&& isset($_POST['_method'])
&& in_array(strtoupper($_POST['_method']), array('PUT', 'PATCH', 'DELETE'))) {
$_SERVER['REQUEST_METHOD'] = strtoupper($_POST['_method']);
unset($_POST['_method']);
}
// Decode URL for accurate matching
$url = urldecode($url);
foreach ($this->urls as $matcher) {
if ($matcher->matches($url)) {
return $matcher->dispatch($url, $args);

View File

@@ -65,9 +65,9 @@ class Draft extends VerySimpleModel {
$draft_body = $draft->getBody();
}
$attrs[] = sprintf('data-draft-original="%s"',
Format::htmlchars(Format::viewableImages($original)));
Format::viewableImages($original, [], true));
return array(Format::htmlchars(Format::viewableImages($draft_body)),
return array(Format::viewableImages($draft_body, [], true),
implode(' ', $attrs));
}
@@ -81,6 +81,7 @@ class Draft extends VerySimpleModel {
foreach ($files as $F) {
$attachments[] = array(
'id' => $F->getId(),
'name' => $F->getName(),
'inline' => true
);
}

View File

@@ -914,7 +914,7 @@ class DynamicFormField extends VerySimpleModel {
/* `variable` is used for automation. Internally it's called `name` */
), "name");
}
if ($this->get('name') && !preg_match('/^(?!\d)([[:alnum:]]|_|-)+$/u', $this->get('name')))
if ($this->get('name') && !preg_match('/^(?!\d)([[:alnum:]]|_)+$/u', $this->get('name')))
$this->addError(__(
'Invalid character in variable name. Please use letters and numbers only.'
), 'name');
@@ -1215,7 +1215,7 @@ class DynamicFormEntry extends VerySimpleModel {
return $entries[$ticket_id];
}
function forTask($id, $force=false) {
static function forTask($id, $force=false) {
static $entries = array();
if (!isset($entries[$id]) || $force) {
$stuff = DynamicFormEntry::objects()->filter(array(
@@ -1355,7 +1355,7 @@ class DynamicFormEntry extends VerySimpleModel {
//use getChanges if getClean returns an empty array
$fieldClean = $field->getClean() ?: $field->getChanges();
if (is_array($fieldClean) && $fieldClean[0])
$fieldClean = json_decode($fieldClean[0], true);
$fieldClean = is_string($fieldClean[0]) ? json_decode($fieldClean[0], true) : $fieldClean[0];
} else
$fieldClean = $field->getClean();
@@ -1593,7 +1593,7 @@ class SelectionField extends FormField {
$selection = array();
if ($value && !is_array($value))
$value = array($value);
$value = JsonDataParser::parse($value) ?: array($value);
if ($value && is_array($value)) {
foreach ($value as $k=>$v) {
@@ -1633,6 +1633,7 @@ class SelectionField extends FormField {
$values = array();
$choices = $this->getChoices();
foreach (explode(',', $value) as $V) {
$V = trim($V);
if (isset($choices[$V]))
$values[$V] = $choices[$V];
}
@@ -1733,6 +1734,7 @@ class SelectionField extends FormField {
?: __('Unknown or invalid input');
}
} elseif ($config['typeahead']
&& $entry
&& ($entered = $this->getWidget()->getEnteredValue())
&& !in_array($entered, $entry)
&& $entered != $entry) {
@@ -1916,7 +1918,7 @@ class SelectionField extends FormField {
$name = $name ?: $this->get('name');
$val = $value;
if ($value && is_array($value))
$val = '"?'.implode('("|,|$)|"?', array_keys($value)).'("|,|$)';
$val = '"?(?<![0-9])'.implode('("|,|$)|"?(?<![0-9])', array_keys($value)).'("|,|$)';
switch ($method) {
case '!includes':
return Q::not(array("{$name}__regex" => $val));

View File

@@ -17,6 +17,7 @@ include_once(INCLUDE_DIR.'class.dept.php');
include_once(INCLUDE_DIR.'class.mail.php');
include_once(INCLUDE_DIR.'class.mailer.php');
include_once(INCLUDE_DIR.'class.oauth2.php');
include_once(INCLUDE_DIR.'class.mime.php');
include_once(INCLUDE_DIR.'class.mailfetch.php');
include_once(INCLUDE_DIR.'class.mailparse.php');
include_once(INCLUDE_DIR.'api.tickets.php');
@@ -334,12 +335,12 @@ class Email extends VerySimpleModel {
if ($errors) return false;
// Update basic settings
$this->email = $vars['email'];
$this->email = Format::sanitize($vars['email']);
$this->name = Format::striptags($vars['name']);
$this->dept_id = $vars['dept_id'];
$this->priority_id = isset($vars['priority_id']) ? $vars['priority_id'] : '0';
$this->topic_id = $vars['topic_id'];
$this->noautoresp = $vars['noautoresp'];
$this->dept_id = (int) $vars['dept_id'];
$this->priority_id = (int) (isset($vars['priority_id']) ? $vars['priority_id'] : 0);
$this->topic_id = (int) $vars['topic_id'];
$this->noautoresp = (int) $vars['noautoresp'];
$this->notes = Format::sanitize($vars['notes']);
if ($this->save())
@@ -471,18 +472,36 @@ class EmailAccount extends VerySimpleModel {
}
public function isActive() {
return $this->active;
return ($this->active && $this->hasCredentials());
}
// **** Don't use it ****
// This routine is depricated and will be removed in the future - OAuth2
// Plugin uses it to check if the email accout has auth2 backend.
public function isEnabled() {
return $this->isOAuthAuth();
}
public function isAuthBackendEnabled() {
return $this->isOAuthAuth()
? (($i=$this->getOAuth2Instance()) && $i->isEnabled())
: true;
}
public function isStrict() {
return $this->getConfig()->getStrictMatching();
}
public function checkStrictMatching($token=null) {
$token ??= $this->getAccessToken();
return ($token && $token->isMatch(
$this->getEmail()->getEmail(),
$this->isStrict()));
}
public function shouldAuthorize() {
// check status and make sure it's oauth
if (!$this->isEnabled() || !$this->isOAuthAuth())
if (!$this->isAuthBackendEnabled() || !$this->isOAuthAuth())
return false;
return (!($cred=$this->getFreshCredentials())
@@ -490,7 +509,10 @@ class EmailAccount extends VerySimpleModel {
// changed somehow
|| !($token=$cred->getAccessToken($this->getConfigSignature()))
// Check if expired
|| $token->isExpired());
|| $token->isExpired()
// If Strict Matching is enabled ensure the email matches
// the Resource Owner
|| !$this->checkStrictMatching($token));
}
@@ -515,7 +537,8 @@ class EmailAccount extends VerySimpleModel {
$id = sprintf('%s:%d',
$this->getAuthBk(), $this->getId());
if ($this->isOAuthAuth())
$id .= sprintf(':%d', $this->getAuthId());
$id .= sprintf(':%d:%b',
$this->getAuthId(), $this->isStrict()); #TODO: Remove strict and delegate to email account
$this->bkId = $id;
}
@@ -530,6 +553,15 @@ class EmailAccount extends VerySimpleModel {
return $this->email;
}
public function getName() {
return $this->getEmail()->getName();
}
public function getAccessToken() {
$cred = $this->getFreshCredentials();
return $cred ? $cred->getAccessToken($this->getConfigSignature()) : null;
}
private function getOAuth2Backend($auth=null) {
$auth = $auth ?: $this->getAuthBk();
return Oauth2AuthorizationBackend::getBackend($auth);
@@ -543,6 +575,7 @@ class EmailAccount extends VerySimpleModel {
'name' => sprintf('%s (%s)',
$email->getEmail(), $this->getType()),
'isactive' => 1,
'strict_matching' => $this->isStrict(),
'notes' => sprintf(
__('OAuth2 Authorization for %s'), $email->getEmail()),
];
@@ -617,7 +650,7 @@ class EmailAccount extends VerySimpleModel {
$this->getId());
}
private function getConfig() {
protected function getConfig() {
if (!isset($this->config))
$this->config = new EmailAccountConfig($this->getNamespace());
return $this->config;
@@ -681,6 +714,8 @@ class EmailAccount extends VerySimpleModel {
// Auth backend can be changed on update
$this->auth_bk = $auth;
$this->save();
// Update Strict Matching
$this->getConfig()->setStrictMatching($_POST['strict_matching'] ? 1 : 0);
} elseif (!isset($errors['err'])) {
$errors['err'] = sprintf('%s %s',
__('Error Saving'),
@@ -710,6 +745,10 @@ class EmailAccount extends VerySimpleModel {
return $this->logActivity($error);
}
public function hasCredentials() {
return ($this->getFreshCredentials());
}
private function getCredentialsVars($auth=null) {
$vars = [];
if (($cred = $this->getCredentials($auth)))
@@ -730,92 +769,202 @@ class EmailAccount extends VerySimpleModel {
return [];
if (!isset($this->cred) || $refresh) {
$cred = null;
$this->cred = $cred = null;
$auth = $auth ?: $this->getAuthBk();
list($type, $provider) = explode(':', $auth);
switch ($type) {
case 'mailbox':
if (($mb=$this->email->getMailBoxAccount())
&& $mb->getAuthBk())
$cred = $mb->getCredentials($mb->getAuthBk(), $refresh);
break;
case 'none':
// No authentication required (open replay)
$cred = new osTicket\Mail\NoAuthCredentials([
'username' => $this->email->getEmail()]);
break;
case 'basic':
if (($c=$this->getConfig())
&& ($creds=$c->toArray())
&& isset($creds['username'])
&& isset($creds['passwd'])) {
// Decrypt password
$cred = new osTicket\Mail\BasicAuthCredentials([
'username' => $creds['username'],
// Decrypt password
'password' => Crypto::decrypt($creds['passwd'],
SECRET_SALT,
md5($creds['username'].$this->getNamespace()))
]);
}
break;
case 'oauth2':
if (($c=$this->getConfig()) && ($creds=$c->toArray())) {
// Decrypt Access Token
if ($creds['access_token']) {
$creds['access_token'] = Crypto::decrypt(
$creds['access_token'],
SECRET_SALT,
md5($creds['resource_owner_email'].$this->getNamespace())
);
}
// Decrypt Referesh Token
if ($creds['refresh_token']) {
$creds['refresh_token'] = Crypto::decrypt(
$creds['refresh_token'],
SECRET_SALT,
md5($creds['resource_owner_email'].$this->getNamespace())
);
}
$errors = [];
$class = 'osTicket\Mail\OAuth2AuthCredentials';
try {
// Init credentials and see of we need to
// refresh the token
if (($cred=$class::init($creds))
&& ($token=$cred->getToken())
&& ($refresh && $token->isExpired())
&& ($bk=$this->getOAuth2Backend())
&& ($info=$bk->refreshAccessToken( #nolint
$token->getRefreshToken(),
$this->getBkId(), $errors))
&& isset($info['access_token'])
&& $this->updateCredentials($auth,
// Merge new access token with
// already decrypted creds
array_merge($creds, $info), $errors
)) {
return $this->getCredentials($auth, $refresh);
} elseif ($errors) {
$errors['err'] = $errors['refresh_token']
?: __('Referesh Token Expired');
}
} catch (Exception $ex) {
$errors['err'] = $ex->getMessage();
}
if (isset($errors['err']))
$this->logError($errors['err']);
}
break;
default:
throw new Exception(sprintf('%s: %s',
$type, __('Unknown Credential Type')));
try {
switch ($type) {
case 'mailbox':
$cred = $this->getMailBoxCredentials($refresh);
break;
case 'none':
// No authentication required (open replay)
$cred = new osTicket\Mail\NoAuthCredentials([
'username' => $this->email->getEmail()]);
break;
case 'basic':
$cred = $this->getBasicAuthCredentials();
break;
case 'oauth2':
$cred = $this->getOAuth2AuthCredentials($provider, $refresh);
break;
default:
throw new Exception(sprintf('%s: %s',
$type, __('Unknown Credential Type')));
}
// Cache the credentials
$this->cred = $cred;
} catch (Exception $ex) {
// Log the error
$this->logError(sprintf('%s: %s',
__('Credentials'), $ex->getMessage()
));
}
$this->cred = $cred;
}
return $this->cred;
}
private function getMailBoxCredentials($refresh=false) {
if (($mb=$this->email->getMailBoxAccount())
&& $mb->getAuthBk())
return $mb->getCredentials($mb->getAuthBk(), $refresh);
}
private function getBasicAuthCredentials() {
if (($c=$this->getConfig())
&& ($creds=$c->toArray())
&& isset($creds['username'])
&& isset($creds['passwd'])) {
return new osTicket\Mail\BasicAuthCredentials([
'username' => $creds['username'],
// Decrypt password
'password' => Crypto::decrypt($creds['passwd'],
SECRET_SALT,
md5($creds['username'].$this->getNamespace()))
]);
}
}
private function updateBasicAuthCredentials($vars, &$errors) {
// Get current credentials - we need to re-encrypt
// password as username might be changing
$creds = $this->getCredentialsVars('basic');
// password change?
if (!$vars['username']) {
$errors['username'] = __('Username Required');
} elseif (!$vars['passwd'] && !$creds['password']) {
$errors['passwd'] = __('Password Required');
} elseif (($setting=$this->getAccountSetting(true))
&& !$setting->isValid()) {
$errors['err'] = implode(', ', $setting->getErrors());
} elseif ($setting && !$errors) {
// Validate the credentials
try {
$cred = new osTicket\Mail\BasicAuthCredentials([
'username' => $vars['username'],
'password' => $vars['passwd'] ?:
$creds['password'],
]);
if (!$this->validateCredentials($cred))
$errors['err'] = __('Invalid Credentials');
} catch (Exception $ex) {
$errors['err'] = $ex->getMessage();
}
}
if (!$errors) {
// Save credentials and get out of here.
$info = [
// username
'username' => $vars['username'],
// Encrypt password
'passwd' => Crypto::encrypt($vars['passwd'] ?:
$creds['password'], SECRET_SALT,
md5($vars['username'].$this->getNamespace()))
];
if (!$this->getConfig()->updateInfo($info))
$errors['err'] = sprintf('%s: %s',
__('BasicAuth'),
__('Error saving credentials'));
}
return !count($errors);
}
private function getOAuth2AuthCredentials($provider, $refresh=false) {
if (!($c=$this->getConfig()))
return false;
$creds=$c->toArray();
// Decrypt Access Token
if ($creds['access_token']) {
$creds['access_token'] = Crypto::decrypt(
$creds['access_token'],
SECRET_SALT,
md5($creds['resource_owner_email'].$this->getNamespace())
);
}
// Decrypt Referesh Token
if ($creds['refresh_token']) {
$creds['refresh_token'] = Crypto::decrypt(
$creds['refresh_token'],
SECRET_SALT,
md5($creds['resource_owner_email'].$this->getNamespace())
);
}
try {
// Init credentials and see of we need to
// refresh the token
$errors = [];
$auth = sprintf('oauth2:%s', $provider);
$class = 'osTicket\Mail\OAuth2AuthCredentials';
if (($cred=$class::init($creds))
&& ($token=$cred->getToken())
&& ($refresh && $token->isExpired())
&& ($bk=$this->getOAuth2Backend())
&& ($info=$bk->refreshAccessToken( #nolint
$token->getRefreshToken(),
$this->getBkId(), $errors))
&& isset($info['access_token'])
&& $this->updateCredentials($auth,
// Merge new access token with
// already decrypted creds
array_merge($creds, $info), $errors
)) {
return $this->getCredentials($auth, $refresh);
} elseif ($errors) {
// Throw an exception with the error
throw new Exception($errors['refresh_token']
?? __('Referesh Token Expired'));
}
} catch (Exception $ex) {
// rethrow the exception including above.
throw $ex;
}
return $cred;
}
private function updateOAuth2AuthCredentials($provider, $vars, &$errors) {
$err = sprintf('%s_auth_bk', $this->getType());
if (!$vars['access_token']) {
$errors[$err] = __('Access Token Required');
} elseif (!$vars['resource_owner_email']
|| !Validator::is_email($vars['resource_owner_email'])) {
$errors[$err] = __('Resource Owner Required');
} elseif ($this->isStrict()
// When in Strict mode Account Email must match resource owner's
// email. Strict mode can be disabled for a global admin to
// authorized onbehalf of other user accounts or shared mailboxes.
&& strcasecmp($this->getEmail()->getEmail(), $vars['resource_owner_email'])) {
$errors[$err] = sprintf(__('Strict Mode: Expecting Authorization for %s not %s'),
$this->getEmail()->getEmail(),
$vars['resource_owner_email']);
} elseif (!$errors) {
// Encrypt Access Token
$vars['access_token'] = Crypto::encrypt(
$vars['access_token'],
SECRET_SALT,
md5($vars['resource_owner_email'].$this->getNamespace()));
// Encrypt Referesh Token
if ($vars['refresh_token']) {
$vars['refresh_token'] = Crypto::encrypt(
$vars['refresh_token'],
SECRET_SALT,
md5($vars['resource_owner_email'].$this->getNamespace())
);
}
$vars['config_signature'] = $this->getConfigSignature();
// TODO: Validate
if (!$this->getConfig()->updateInfo($vars))
$errors['err'] = sprintf('oauth2:%s - %s',
Format::htmlchars($provider),
__('Error saving credentials'));
}
return !count($errors);
}
public function updateCredentials($auth, $vars, &$errors) {
if (!$vars || $errors)
return false;
@@ -823,78 +972,12 @@ class EmailAccount extends VerySimpleModel {
list($type, $provider) = explode(':', $auth);
switch ($type) {
case 'basic':
// Get current credentials - we need to re-encrypt
// password as username might be changing
$creds = $this->getCredentialsVars($auth);
// password change?
if (!$vars['username']) {
$errors['username'] = __('Username Required');
} elseif (!$vars['passwd'] && !$creds['password']) {
$errors['passwd'] = __('Password Required');
} elseif (($setting=$this->getAccountSetting(true))
&& !$setting->isValid()) {
$errors['err'] = implode(', ', $setting->getErrors());
} elseif ($setting && !$errors) {
// Validate the credentials
try {
$cred = new osTicket\Mail\BasicAuthCredentials([
'username' => $vars['username'],
'password' => $vars['passwd'] ?:
$creds['password'],
]);
if (!$this->validateCredentials($cred))
$errors['err'] = __('Invalid Credentials');
} catch (Exception $ex) {
$errors['err'] = $ex->getMessage();
}
}
if (!$errors) {
// Save credentials and get out of here.
$info = [
// username
'username' => $vars['username'],
// Encrypt password
'passwd' => Crypto::encrypt($vars['passwd'] ?:
$creds['password'], SECRET_SALT,
md5($vars['username'].$this->getNamespace()))
];
if ($this->getConfig()->updateInfo($info))
$this->cred = null;
else
$errors['err'] = sprintf('%s: %s',
Format::htmlchars($type),
__('Error saving credentials'));
}
if (!($this->updateBasicAuthCredentials($vars, $errors)))
return false;
break;
case 'oauth2':
if (!$vars['access_token']) {
$errors['access_token'] = __('Access Token Required');
} elseif (!$vars['resource_owner_email']
|| !Validator::is_email($vars['resource_owner_email'])) {
$errors['resource_owner_email'] =
__('Resource Owner Required');
} elseif (!$errors) {
// Encrypt Access Token
$vars['access_token'] = Crypto::encrypt(
$vars['access_token'],
SECRET_SALT,
md5($vars['resource_owner_email'].$this->getNamespace()));
// Encrypt Referesh Token
if ($vars['refresh_token']) {
$vars['refresh_token'] = Crypto::encrypt(
$vars['refresh_token'],
SECRET_SALT,
md5($vars['resource_owner_email'].$this->getNamespace())
);
}
$vars['config_signature'] = $this->getConfigSignature();
if (!$this->getConfig()->updateInfo($vars))
$errors['err'] = sprintf('%s: %s',
Format::htmlchars($type),
__('Error saving credentials'));
}
if (!($this->updateOAuth2AuthCredentials($provider, $vars, $errors)))
return false;
break;
default:
$errors['err'] = sprintf('%s - %s',
@@ -905,12 +988,20 @@ class EmailAccount extends VerySimpleModel {
if ($errors)
return false;
// Save the auth backend
$this->auth_bk = $auth;
// Clear cached credentials
$this->creds = null;
return $this->save();
}
/*
* Destory the account config
*/
function destroyConfig() {
return $this->getConfig()->destroy();
}
function update($vars, &$errors) {
return false;
}
@@ -923,6 +1014,7 @@ class EmailAccount extends VerySimpleModel {
} else {
$this->num_errors = 0;
$this->last_error = null;
$this->last_error_msg = null;
$this->last_activity = $now ?: SqlFunction::NOW();
}
return $this->save();
@@ -937,7 +1029,7 @@ class EmailAccount extends VerySimpleModel {
function delete() {
// Destroy the Email config
$this->getConfig()->destroy();
$this->destroyConfig();
// Delete the Plugin instance
if ($this->isOAuthAuth() && ($i=$this->getOAuth2Instance()))
$i->delete();
@@ -947,7 +1039,7 @@ class EmailAccount extends VerySimpleModel {
static function create($ht=false) {
$i = new static($ht);
$i->active = 0;
$i->active = isset($ht['active']) ? $ht['active'] : 0;
$i->created = SqlFunction::NOW();
return $i;
}
@@ -981,6 +1073,16 @@ class MailBoxAccount extends EmailAccount {
->filter(['type' => 'mailbox']);
}
//TODO: Morhp MailBoxAccount to ImapMailBoxAccount
public function isImap() {
return (strcasecmp($this->getProtocol(), 'IMAP') === 0);
}
//TODO: Morhp MailBoxAccount to PopMailBoxAccount
public function isPop() {
return (strcasecmp($this->getProtocol(), 'POP') === 0);
}
public function getProtocol() {
return $this->protocol;
}
@@ -989,9 +1091,13 @@ class MailBoxAccount extends EmailAccount {
return $this->folder;
}
public function getFetchFolder() {
return $this->getFolder();
}
public function getArchiveFolder() {
if ((strcasecmp($this->getProtocol(), 'imap') == 0) && $this->archivefolder)
return $this->archivefolder;
return ($this->isImap() && $this->archivefolder)
? $this->archivefolder : null;
}
public function canDeleteEmails() {
@@ -1006,7 +1112,7 @@ class MailBoxAccount extends EmailAccount {
return $this->getMailBox($creds);
}
public function getMailBox(osTicket\Mail\AuthCredentials $cred=null) {
public function getMailBox(?osTicket\Mail\AuthCredentials $cred=null) {
if (!isset($this->mailbox) || $cred) {
$this->cred = $cred ?: $this->getFreshCredentials();
$setting = $this->getAccountSetting();
@@ -1092,6 +1198,7 @@ class MailBoxAccount extends EmailAccount {
$this->fetchmax = $vars['mailbox_fetchmax'] ?: 30;
$this->postfetch = $vars['mailbox_postfetch'];
$this->last_activity = null;
$this->last_error_msg = null;
$this->num_errors = 0;
//Post fetch email handling...
switch ($vars['mailbox_postfetch']) {
@@ -1112,7 +1219,7 @@ class MailBoxAccount extends EmailAccount {
!$mb->hasFolder($this->folder))
$errors['mailbox_folder'] = __('Unknown Folder');
if ($this->archivefolder
&& $this->protocol == 'IMAP'
&& $this->isImap()
&& !$mb->hasFolder($this->archivefolder)
&& !$mb->createFolder($this->archivefolder))
$errors['mailbox_archivefolder'] =
@@ -1177,6 +1284,23 @@ class SmtpAccount extends EmailAccount {
->filter(['type' => 'smtp']);
}
public function isMailboxAuth() {
return (strcasecmp($this->getAuthBk(), 'mailbox') === 0);
}
/*
* Check if using mailbox auth and MailboxAccount exists if so
* return the MailboxAccount config, otherwise return it's own
* config
*/
protected function getConfig() {
if ($this->isMailboxAuth()
&& ($email=$this->getEmail())
&& ($account=$email->getMailBoxAccount()))
return $account->getConfig();
return parent::getConfig();
}
public function allowSpoofing() {
return ($this->allow_spoofing);
}
@@ -1193,16 +1317,18 @@ class SmtpAccount extends EmailAccount {
return $this->smtp;
}
public function getSmtp(osTicket\Mail\AuthCredentials $cred=null) {
public function getSmtp(?osTicket\Mail\AuthCredentials $cred=null) {
if (!isset($this->smtp) || $cred) {
$this->cred = $cred ?: $this->getFreshCredentials();
$setting = $this->getAccountSetting();
$setting->setCredentials($this->cred);
$smtpOptions = new osTicket\Mail\SmtpOptions($setting);
$smtp = new osTicket\Mail\Smtp($smtpOptions);
// Attempt to connect if Credentials are sent
if ($cred) $smtp->connect();
$this->smtp = $smtp;
if ($this->cred) {
$setting = $this->getAccountSetting();
$setting->setCredentials($this->cred);
$smtpOptions = new osTicket\Mail\SmtpOptions($setting);
$smtp = new osTicket\Mail\Smtp($smtpOptions);
// Attempt to connect now if credentials are sent in
if ($cred) $smtp->connect();
$this->smtp = $smtp;
}
}
return $this->smtp;
}
@@ -1228,6 +1354,14 @@ class SmtpAccount extends EmailAccount {
&& !($creds=$this->getFreshCredentials($vars['smtp_auth_bk'])))
$_errors['smtp_auth_bk'] = __('Configure Authentication');
// Check if set to active and using mailbox auth, if so check strict
// matching.
if ($vars['smtp_active'] == 1
&& ($vars['smtp_auth_bk'] === 'mailbox')
&& (strpos($vars['mailbox_auth_bk'], 'oauth2') === 0)
&& !$this->checkStrictMatching())
$_errors['smtp_auth_bk'] = sprintf('%s and %s', __('Resource Owner'), __('Email Mismatch'));
if (!$_errors) {
$this->active = $vars['smtp_active'] ? 1 : 0;
$this->host = $vars['smtp_host'];
@@ -1236,6 +1370,7 @@ class SmtpAccount extends EmailAccount {
$this->protocol = 'SMTP';
$this->allow_spoofing = $vars['smtp_allow_spoofing'] ? 1 : 0;
$this->last_activity = null;
$this->last_error_msg = null;
$this->num_errors = 0;
// If account is active then attempt to authenticate
if ($this->active && $creds) {
@@ -1272,6 +1407,20 @@ class SmtpAccount extends EmailAccount {
*
*/
class EmailAccountConfig extends Config {
/*
* Get strict matching (default: true)
*/
public function getStrictMatching() {
return $this->get('strict_matching', true);
}
/*
* Set strict matching
*/
public function setStrictMatching($mode) {
return $this->set('strict_matching', !!$mode);
}
public function updateInfo($vars) {
return parent::updateAll($vars);
}

View File

@@ -259,7 +259,7 @@ class Export {
// Filename or stream to export agents to
$filename = $filename ?: sprintf('Agents-%s.csv',
strftime('%Y%m%d'));
date('Ymd'));
Http::download($filename, "text/$how");
$depts = Dept::getDepartments(null, true, Dept::DISPLAY_DISABLED);
echo self::dumpQuery($agents, array(
@@ -296,7 +296,7 @@ static function departmentMembers($dept, $agents, $filename='', $how='csv') {
// Filename or stream to export depts' agents to
$filename = $filename ?: sprintf('%s-%s.csv', $dept->getName(),
strftime('%Y%m%d'));
date('Ymd'));
Http::download($filename, "text/$how");
echo self::dumpQuery($agents, array(
'::getName' => 'Name',
@@ -322,7 +322,7 @@ static function departmentMembers($dept, $agents, $filename='', $how='csv') {
exit;
}
static function audits($type, ?string $filename=null, ?string $tableInfo=null, ?string $object=null, ?string $how='csv', ?bool $show_viewed=true, ?array $data=array(), CsvExporter $exporter) {
static function audits($type, ?string $filename, ?string $tableInfo, ?string $object, ?string $how='csv', ?bool $show_viewed=true, ?array $data=array(), CsvExporter $exporter) {
$headings = array('Description', 'Timestamp', 'IP');
switch ($type) {
case 'audit':
@@ -535,7 +535,7 @@ abstract class Exporter {
|| !($email=$cfg->getDefaultEmail()))
return false;
$mailer = new Mailer($email);
$mailer = new osTicket\Mail\Mailer($email);
$mailer->addFileObject($file);
$subject = __("Export");
$body = __("Attached is file containing the export you asked us to send you!");
@@ -620,7 +620,7 @@ class CsvExporter extends Exporter {
}
function write($data) {
fputcsv($this->fp, $this->escape($data), $this->getDelimiter());
fputcsv($this->fp, $this->escape($data), $this->getDelimiter(), "\"", "");
}
}
@@ -725,7 +725,7 @@ class CsvResultsExporter extends ResultSetExporter {
$delimiter = $this->getDelimiter();
// Output a UTF-8 BOM (byte order mark)
fputs($this->output, chr(0xEF) . chr(0xBB) . chr(0xBF));
fputcsv($this->output, $this->getHeaders(), $delimiter);
fputcsv($this->output, $this->getHeaders(), $delimiter, "\"", "");
while ($row=$this->next())
fputcsv($this->output, array_map(
function($v){
@@ -733,7 +733,7 @@ class CsvResultsExporter extends ResultSetExporter {
return "'".$v;
return $v;
}, $row),
$delimiter);
$delimiter, "\"", "");
if (!$tmp)
fclose($this->output);

View File

@@ -347,7 +347,7 @@ class FAQ extends VerySimpleModel {
static function getFeatured() {
return self::objects()
->filter(array('ispublished__in'=>array(1,2), 'category__ispublic'=>1))
->filter(array('ispublished'=>2, 'category__ispublic__in'=>[1,2]))
->order_by('-ispublished');
}

View File

@@ -15,6 +15,19 @@ require_once(INCLUDE_DIR.'class.signal.php');
require_once(INCLUDE_DIR.'class.error.php');
/**
* FileObject Interface
*
* Methods File Objects should support
**/
interface FileObjectInterface {
function getUId();
function getKey();
function getName();
function getData();
function getMimeType();
}
/**
* Represents a file stored in a storage backend. It is generally attached
* to something; however company logos, login page backdrops, and other
@@ -29,8 +42,8 @@ require_once(INCLUDE_DIR.'class.error.php');
* - 'L' => Logo
* - 'B' => Backdrop
*/
class AttachmentFile extends VerySimpleModel {
class AttachmentFile extends VerySimpleModel
implements FileObjectInterface {
static $meta = array(
'table' => FILE_TABLE,
'pk' => array('id'),
@@ -71,6 +84,10 @@ class AttachmentFile extends VerySimpleModel {
return $this->id;
}
function getUId() {
return $this->getId();
}
function getType() {
return $this->type;
}
@@ -158,6 +175,10 @@ class AttachmentFile extends VerySimpleModel {
}
function display($scale=false, $ttl=86400) {
// Force download if not safe viewable image
if (!$this->isInlineSafeImage())
return $this->download(false, 'attachment');
$this->makeCacheable($ttl);
if ($scale && extension_loaded('gd')
@@ -197,6 +218,12 @@ class AttachmentFile extends VerySimpleModel {
'type' => $options['type']))))
$options['id'] = $a->getId();
// Force attachment disposition if not inline-safe image
if (isset($options['disposition'])
&& strcasecmp($options['disposition'], 'inline') == 0
&& !$this->isInlineSafeImage())
$options['disposition'] = 'attachment';
return static::generateDownloadUrl($this->getId(),
strtolower($this->getKey()), $this->getSignature(),
$options);
@@ -250,7 +277,8 @@ class AttachmentFile extends VerySimpleModel {
$check = static::_genUrlSignature($this->getId(), $this->getKey(),
$this->getSignature(), $expires);
return $signature == $check;
return hash_equals($check, $signature);
}
static function _genUrlSignature($id, $key, $signature, $expires) {
@@ -270,7 +298,7 @@ class AttachmentFile extends VerySimpleModel {
$inline = ($thisstaff ? ($thisstaff->getImageAttachmentView() === 'inline') : false);
$disposition = ((($disposition && strcasecmp($disposition, 'inline') == 0)
|| $inline)
&& strpos($this->getType(), 'image/') !== false)
&& $this->isInlineSafeImage())
? 'inline' : 'attachment';
$ttl = ($expires) ? $expires - Misc::gmtime() : false;
$bk = $this->open();
@@ -349,6 +377,7 @@ class AttachmentFile extends VerySimpleModel {
case IMAGETYPE_GIF:
case IMAGETYPE_JPEG:
case IMAGETYPE_PNG:
case IMAGETYPE_WEBP:
break;
default:
$error = __('Invalid image file type');
@@ -370,6 +399,7 @@ class AttachmentFile extends VerySimpleModel {
case IMAGETYPE_GIF:
case IMAGETYPE_JPEG:
case IMAGETYPE_PNG:
case IMAGETYPE_WEBP:
break;
default:
$error = __('Invalid image file type');
@@ -702,6 +732,29 @@ class AttachmentFile extends VerySimpleModel {
->filter(array('ft' => 'B'))
->order_by('created');
}
/**
* Determines whether a MIME type is safe to render inline as a passive image.
*
* @param string $type
* @return bool
*/
static function isInlineSafeImageType($type) {
$type = strtolower(trim((string) $type));
return strpos($type, 'image/') === 0
&& $type !== 'image/svg+xml'
&& $type !== 'image/svg';
}
/**
* Determines whether this file is safe to render inline as a passive image.
*
* @return bool
*/
function isInlineSafeImage() {
return static::isInlineSafeImageType($this->getType());
}
}
class FileStorageBackend {
@@ -1029,14 +1082,29 @@ class OneSixAttachments extends FileStorageBackend {
FileStorageBackend::register('6', 'OneSixAttachments');
// FileObject - wrapper for SplFileObject class
class FileObject extends SplFileObject {
class FileObject extends SplFileObject
implements FileObjectInterface {
protected $_key;
protected $_sig;
protected $_filename;
function __construct($file, $mode='r') {
parent::__construct($file, $mode);
}
function getUId() {
return $this->getKey();
}
function getKey() {
if (!isset($this->_key))
list($this->_key, $this->_sig) = AttachmentFile::_getKeyAndHash(
$this->getContents());
return $this->_key;
}
/* This allows us to set REAL file name as opposed to basename of the
* FS file in question
*/
@@ -1048,6 +1116,10 @@ class FileObject extends SplFileObject {
return $this->_filename ?: parent::getFilename();
}
function getName() {
return $this->getFilename();
}
/*
* Set mime type - well formated mime is expected.
*/
@@ -1056,12 +1128,8 @@ class FileObject extends SplFileObject {
}
function getMimeType() {
if (!isset($this->_mimetype)) {
// Try to to auto-detect mime type
$finfo = new finfo(FILEINFO_MIME);
$this->_mimetype = $finfo->buffer($this->getContents(),
FILEINFO_MIME_TYPE);
}
if (!isset($this->_mimetype))
$this->_mimetype = self::mime_type($this->getRealPath());
return $this->_mimetype;
}
@@ -1077,6 +1145,28 @@ class FileObject extends SplFileObject {
function getData() {
return $this->getContents();
}
/*
* Given a filepath - auto detect the mime type
*
*/
static function mime_type($filepath) {
// Try to to auto-detect mime type
$type = null;
if (function_exists('finfo_open')) {
$finfo = finfo_open(FILEINFO_MIME_TYPE);
$type = finfo_file($finfo, $filepath);
finfo_close($finfo);
}
return $type ?: mime_content_type($filepath);
}
/*
* Compare mime type of file content to a given mime
*/
static function mimecmp($filepath, $mime) {
return strcasecmp(self::mime_type($filepath), $mime) !== 0;
}
}
?>

View File

@@ -915,6 +915,7 @@ class TicketFilter {
);
foreach ($auto_headers as $header=>$find) {
$header = strtolower($header);
if(!isset($headers[$header])) continue;
$value = strtoupper($headers[$header]);
@@ -938,8 +939,7 @@ class TicketFilter {
}
static function isBounce($headers) {
if($headers && !is_array($headers))
if ($headers && !is_array($headers))
$headers = Mail_Parse::splitHeaders($headers);
$bounce_headers = array(
@@ -954,6 +954,7 @@ class TicketFilter {
);
foreach ($bounce_headers as $header => $find) {
$header = strtolower($header);
if(!isset($headers[$header])) continue;
@list($func, $searches, $pos, $neg) = $find;

View File

@@ -89,7 +89,7 @@ class FilterAction extends VerySimpleModel {
return $this->_impl;
}
static function setFilterFlags(?object $actions=null, $flag, $bool) {
static function setFilterFlags(?object $actions, $flag, $bool) {
$flag = constant($flag);
if ($actions)
foreach ($actions as $action)
@@ -642,7 +642,7 @@ class FA_SendEmail extends TriggerAction {
// Honor FROM address settings
if (!$config['from'] || !($mailer = Email::lookup($config['from'])))
$mailer = new Mailer();
$mailer = new osTicket\Mail\Mailer();
// Allow %{user} in the To: line
$replacer = new VariableReplacer();

View File

@@ -278,6 +278,11 @@ class Format {
$props = array();
foreach ($styles as $i=>&$s) {
@list($prop, $val) = explode(':', $s);
// Strip all CSS url() usages in inline styles
if (preg_match('/\burl\s*\(/i', $val)) {
unset($styles[$i]);
continue;
}
if (isset($props[$prop])) {
unset($styles[$i]);
continue;
@@ -334,9 +339,10 @@ class Format {
':<html[^>]+:i', # drop html attributes
':<(a|span) (name|style)="(mso-bookmark\:)?_MailEndCompose">(.+)?<\/(a|span)>:', # Drop _MailEndCompose
':<div dir=(3D)?"ltr">(.*?)<\/div>(.*):is', # drop Gmail "ltr" attributes
':data-cid="[^"]*":', # drop image cid attributes
'(position: ?(-webkit-)?(static|relative|fixed|absolute|sticky|initial|inherit);?)', # Position styling
':[\x{2002}-\x{200B}]+:u', # unicode spaces
),
array('', '', '', '', '<html', '$4', '$2 $3', ''),
array('', '', '', '', '<html', '$4', '$2 $3', '', ' '),
$html);
// HtmLawed specific config only
@@ -346,7 +352,7 @@ class Format {
'comment' => 1, //Remove html comments (OUTLOOK LOVE THEM)
'tidy' => -1,
'elements' => '*-form-input-button',
'deny_attribute' => 'id, formaction, action, on*',
'deny_attribute' => 'id, formaction, action, srcset, data*, on*',
'schemes' => 'href: aim, feed, file, ftp, gopher, http, https, irc, mailto, news, nntp, sftp, ssh, telnet; *:file, http, https; src: cid, http, https, data',
'hook_tag' => function($e, $a=0) { return Format::__html_cleanup($e, $a); },
);
@@ -355,10 +361,13 @@ class Format {
if ($cfg)
$whitelist = $cfg->getIframeWhitelist();
if (!empty($whitelist)) {
$whitelist_quoted = array_map(function ($d) {
return preg_quote($d, '`');
}, $whitelist);
$config['elements'] .= '+iframe';
$config['spec'] = 'iframe=-*,height,width,type,style,src(match="`^(https?:)?//(www\.)?('
.implode('|', $whitelist)
.')/?([^@]*)$`i"),frameborder'.($options['spec'] ? '; '.$options['spec'] : '').',allowfullscreen';
.implode('|', $whitelist_quoted)
.')(\?|/|#)([^@]*)$`i"),frameborder'.($options['spec'] ? '; '.$options['spec'] : '').',allowfullscreen';
}
return Format::html($html, $config);
@@ -367,17 +376,17 @@ class Format {
static function localizeInlineImages($text) {
// Change file.php urls back to content-id's
return preg_replace(
'`src="(?:https?:/)?(?:/[^/"]+)*?/file\\.php\\?(?:\w+=[^&]+&(?:amp;)?)*?key=([^&]+)[^"]*`',
'src="cid:$1', $text);
'`<img src="(?:https?:/)?(?:/[^/"]+)*?/file\\.php\\?(?:\w+=[^&"]+&(?:amp;)?)*?key=([^&]+)[^"]*`',
'<img src="cid:$1', $text);
}
static function sanitize($text, $striptags=false, $spec=false) {
// Localize inline images before sanitizing content
$text = self::localizeInlineImages($text);
//balance and neutralize unsafe tags.
$text = Format::safe_html($text, array('spec' => $spec));
$text = self::localizeInlineImages($text);
//If requested - strip tags with decoding disabled.
return $striptags?Format::striptags($text, false):$text;
}
@@ -422,6 +431,14 @@ class Format {
return htmlspecialchars_decode($var, $flags);
}
static function http_query_string(string $query, ?array $filter = null) {
$args = [];
parse_str($query, $args);
if ($filter && is_array($filter))
$args = array_diff_key($args, array_flip($filter));
return http_build_query($args);
}
static function input($var) {
return Format::htmlchars($var);
}
@@ -473,16 +490,17 @@ class Format {
// Allowed Inline External Image Extensions
$allowed = array('gif', 'png', 'jpg', 'jpeg');
$exclude = !$cfg->allowExternalImages();
$local = false;
$input = preg_replace_callback('/<img ([^>]*)(src="([^"]+)")([^>]*)\/?>/',
function($match) use ($local, $allowed, $exclude, $display) {
if (strpos($match[3], 'cid:') !== false)
$local = true;
$input = preg_replace_callback('/<img\b([^>]*?)\bsrc\s*=\s*(["\'])(.*?)\2([^>]*)\/?>/i',
function($match) use ($allowed, $exclude, $display) {
$src = $match[3];
// CID is only valid if it is the prefix of the src value
$local = (stripos(ltrim($src), 'cid:') === 0);
// Split the src URL and get the extension
$part = parse_url($match[3], PHP_URL_PATH);
$path = explode('.', $part);
$part = parse_url($src, PHP_URL_PATH);
$path = explode('.', (string) $part);
$ext = preg_split('/[^A-Za-z]/', end($path))[0];
if (!$local && (($exclude && $display) || !in_array($ext, $allowed)))
@@ -490,7 +508,8 @@ class Format {
else
return $match[0];
},
$input);
$input
);
return $input;
}
@@ -527,7 +546,8 @@ class Format {
'/[\x{23E0}-\x{23EF}]/u', # More Buttons
'/[\x{2310}-\x{231F}]/u', # Hourglass/Watch
'/[\x{1000B6}]/u', # Private Use Area (Plane 16)
'/[\x{2322}-\x{232F}]/u' # Keyboard
'/[\x{2322}-\x{232F}]/u', # Keyboard
'/\x{00B0}|\x{00BA}/u' # Degree Symbol
), '', $text);
}
@@ -575,18 +595,22 @@ class Format {
}
static function viewableImages($html, $options=array()) {
static function viewableImages($html, $options=array(), $format=false) {
$cids = $images = array();
$options +=array(
'disposition' => 'inline');
return preg_replace_callback('/"cid:([\w._-]{32})"/',
$html = preg_replace_callback('/("|&quot;)cid:([\w._-]{32})("|&quot;)/',
function($match) use ($options, $images) {
if (!($file = AttachmentFile::lookup($match[1])))
if (!($file = AttachmentFile::lookup($match[2])))
return $match[0];
if (!$file->isInlineSafeImage())
return $match[0];
return sprintf('"%s" data-cid="%s"',
$file->getDownloadUrl($options), $match[1]);
$file->getDownloadUrl($options), $match[2]);
}, $html);
return $format ? Format::htmlchars($html, true) : $html;
}
@@ -747,6 +771,7 @@ class Format {
if ($cfg && $cfg->isForce24HourTime())
$format = str_replace('X', 'R', $format);
// TODO: Deprecated; replace this soon
return strftime($format, $timestamp);
}
@@ -1021,7 +1046,7 @@ class Format {
// Performs Unicode normalization (where possible) and splits words at
// difficult word boundaries (for far eastern languages)
static function searchable($text, $lang=false) {
static function searchable($text, $lang=false, $length=false) {
global $cfg;
if (function_exists('normalizer_normalize')) {
@@ -1053,6 +1078,10 @@ class Format {
// Drop leading and trailing whitespace
$text = trim($text);
}
if ($length && (str_word_count($text) > $length))
return null;
return $text;
}

View File

@@ -1541,7 +1541,7 @@ class TextboxField extends FormField {
function($v) use ($config) {
$regex = $config['regex'];
return @preg_match($regex, $v);
}, __('Value does not match required pattern')
}, $config['validator-error'] ?? __('Value does not match required pattern')
),
);
// Support configuration forms, as well as GUI-based form fields
@@ -1789,6 +1789,10 @@ class BooleanField extends FormField {
return ($value) ? __('Yes') : __('No');
}
function asVar($value, $id=false) {
return $this->toString($value);
}
function getClean($validate=true) {
if (!isset($this->_clean)) {
$this->_clean = (isset($this->value))
@@ -2085,7 +2089,7 @@ class ChoiceField extends FormField {
$name = $name ?: $this->get('name');
$val = $value;
if ($value && is_array($value))
$val = '"?'.implode('("|,|$)|"?', array_keys($value)).'("|,|$)';
$val = '"?(?<![0-9])'.implode('("|,|$)|"?(?<![0-9])', array_keys($value)).'("|,|$)';
switch ($method) {
case '!includes':
return Q::not(array("{$name}__regex" => $val));
@@ -2565,26 +2569,26 @@ class DatetimeField extends FormField {
"{$name}__lte" => $right->format('Y-m-d H:i:s'),
));
case 'ndaysago':
$int = $intervals[$value['int'] ?: 'd'] ?: 'DAY';
$int = $intervals[(string) $value['int'] ?: 'd'] ?: 'DAY';
$interval = new SqlInterval($int, $value['until']);
return new Q(array(
"{$name}__range" => array($now->minus($interval), $now),
));
case 'ndays':
$int = $intervals[$value['int'] ?: 'd'] ?: 'DAY';
$int = $intervals[(string) $value['int'] ?: 'd'] ?: 'DAY';
$interval = new SqlInterval($int, $value['until']);
return new Q(array(
"{$name}__range" => array($now, $now->plus($interval)),
));
// Distant past and future ranges
case 'distpast':
$int = $intervals[$value['int'] ?: 'd'] ?: 'DAY';
$int = $intervals[(string) $value['int'] ?: 'd'] ?: 'DAY';
$interval = new SqlInterval($int, $value['until']);
return new Q(array(
"{$name}__lte" => $now->minus($interval),
));
case 'distfut':
$int = $intervals[$value['int'] ?: 'd'] ?: 'DAY';
$int = $intervals[(string) $value['int'] ?: 'd'] ?: 'DAY';
$interval = new SqlInterval($int, $value['until']);
return new Q(array(
"{$name}__gte" => $now->plus($interval),
@@ -2761,6 +2765,14 @@ class SectionBreakField extends FormField {
function isBlockLevel() {
return true;
}
function isEditableToStaff() {
return $this->isVisibleToStaff();
}
function isEditableToUsers() {
return $this->isVisibleToUsers();
}
}
class ThreadEntryField extends FormField {
@@ -3324,7 +3336,7 @@ class DepartmentField extends ChoiceField {
function getValue() {
if (($value = parent::getValue()) && ($id=$this->getClean()))
return $value[$id];
return is_array($value) ? $value[$id] : $value;
}
function to_php($value, $id=false) {
@@ -3868,6 +3880,10 @@ class FileUploadField extends FormField {
'hint'=>__('Optionally, enter comma-separated list of additional file types, by extension. (e.g .doc, .pdf).'),
'configuration'=>array('html'=>false, 'rows'=>2),
)),
'strictmimecheck' => new BooleanField([
'id' => 4, 'label'=>__('Strict Mime Type Check'), 'required' => false, 'default' => false,
'hint' => 'File Mime Type associations is OS dependent',
'configuration' => ['desc' => __('Enable strict Mime Type check')]]),
'max' => new TextboxField(array(
'label'=>__('Maximum Files'),
'hint'=>__('Users cannot upload more than this many files.'),
@@ -3892,8 +3908,9 @@ class FileUploadField extends FormField {
Http::response(400, 'Send one file at a time');
$file = array_shift($files);
$file['name'] = urldecode($file['name']);
$config = $this->getConfiguration();
if (!$this->isValidFile($file))
if (!self::isValidFile($file, $config['strictmimecheck']))
Http::response(413, 'Invalid File');
if (!$bypass && !$this->isValidFileType($file['name'], $file['type']))
@@ -3922,10 +3939,11 @@ class FileUploadField extends FormField {
if (!$this->isValidFileType($file['name'], $file['type']))
throw new FileUploadError(__('File type is not allowed'));
if (!$this->isValidFile($file))
$config = $this->getConfiguration();
if (!self::isValidFile($file, $config['strictmimecheck']))
throw new FileUploadError(__('Invalid File'));
$config = $this->getConfiguration();
if ($file['size'] > $config['size'])
throw new FileUploadError(__('File size is too large'));
@@ -3962,13 +3980,25 @@ class FileUploadField extends FormField {
return $F;
}
function isValidFile($file) {
/**
* Strict mode can be enabled in Admin Panel > Settings > Tickets
*
* PS: Please note that the a mismatch can happen if the mime types
* database is not up to date or a little different compared to what the
* browser reports.
**/
static function isValidFile($file, $strict = false) {
// Strict mime check
if ($strict
&& !empty($file['type'])
&& FileObject::mimecmp($file['tmp_name'], $file['type']))
return false;
// Check invalid image hacks
if ($file['tmp_name']
&& stripos($file['type'], 'image/') === 0
&& function_exists('exif_imagetype')
&& !exif_imagetype($file['tmp_name']))
&& !(exif_imagetype($file['tmp_name'])
|| mime_content_type($file['tmp_name']) === 'image/svg+xml'))
return false;
return true;
@@ -4025,7 +4055,11 @@ class FileUploadField extends FormField {
}
function getConfiguration() {
global $cfg;
$config = parent::getConfiguration();
// If no size present default to system setting
$config['size'] ??= $cfg->getMaxFileSize();
$_types = self::getFileTypes();
$mimetypes = array();
$extensions = array();
@@ -4085,7 +4119,7 @@ class FileUploadField extends FormField {
if (isset($this->attachments) && $this->attachments) {
$this->attachments->keepOnlyFileIds($value);
}
return JsonDataEncoder::encode($value);
return JsonDataEncoder::encode($value) ?? NULL;
}
function parse($value) {
@@ -5214,6 +5248,8 @@ class FileUploadWidget extends Widget {
);
function render($options=array()) {
global $ost;
$config = $this->field->getConfiguration();
$name = $this->field->getFormName();
$id = substr(md5(spl_object_hash($this)), 10);
@@ -5277,7 +5313,7 @@ class FileUploadWidget extends Widget {
// Get Form Type
$type = $this->field->getForm()->type;
// Determine if for Ticket/Task/Custom
if ($type) {
if ($type && !is_numeric($field_id)) {
if ($type == 'T')
$field_id = 'ticket/attach';
elseif ($type == 'A')
@@ -5299,6 +5335,7 @@ class FileUploadWidget extends Widget {
url: 'ajax.php/form/upload/<?php echo $field_id; ?>',
link: $('#<?php echo $id; ?>').find('a.manual'),
paramname: 'upload[]',
data: {"__CSRFToken__": "<?php echo $ost->getCSRF()->getToken(); ?>"},
fallback_id: 'file-<?php echo $id; ?>',
allowedfileextensions: <?php echo JsonDataEncoder::encode(
$config['__extensions'] ?: array()); ?>,
@@ -5401,6 +5438,14 @@ class FreeTextField extends FormField {
return true;
}
function isEditableToStaff() {
return $this->isVisibleToStaff();
}
function isEditableToUsers() {
return $this->isVisibleToUsers();
}
/* utils */
function to_config($config) {
@@ -6099,7 +6144,7 @@ class TransferForm extends Form {
'refer' => new BooleanField(array(
'id'=>2, 'label'=>'', 'required'=>false, 'default'=>false,
'configuration'=>array(
'desc' => 'Maintain referral access to current department')
'desc' => __('Maintain referral access to current department'))
)),
'comments' => new TextareaField(array(
'id' => 3,
@@ -6141,7 +6186,7 @@ class TransferForm extends Form {
break;
default:
throw new Exception(sprintf(__('%s: Unknown template style %s'),
get_class(), $options['template']));
get_class($this), $options['template']));
}
$form = $this;

View File

@@ -113,10 +113,6 @@ class Http {
if (false !== strpos($user_agent,'msie')
&& false !== strpos($user_agent,'win'))
return 'filename='.rawurlencode($filename);
elseif (false !== strpos($user_agent, 'safari')
&& false === strpos($user_agent, 'chrome'))
// Safari and Safari only can handle the filename as is
return 'filename='.str_replace(',', '', $filename);
else
// Use RFC5987
return "filename*=UTF-8''".rawurlencode($filename);
@@ -151,5 +147,59 @@ class Http {
return http_build_query($vars, '', $separator);
}
static function domain() {
$domain = null;
if (isset($_SERVER['HTTP_HOST'])
&& strpos($_SERVER['HTTP_HOST'], '.') !== false
&& !Validator::is_ip($_SERVER['HTTP_HOST']))
// Remote port specification, as it will make an invalid domain
list($domain) = explode(':', $_SERVER['HTTP_HOST']);
return $domain;
}
// Get current url
static function url() {
// Scheme + Host.
$https = osTicket::is_https();
$url = sprintf('http%s://%s',
$https ? 's' : '',
$_SERVER['HTTP_HOST']);
// Add Port if not 80 and not https
$port = osTicket::get_client_port();
if ($port && $port != 80 && !$https && !str_contains($url, $port))
$url .= ":{$port}";
// Path + Query string
if (isset($_SERVER['REQUEST_URI']))
$url .= $_SERVER['REQUEST_URI'];
else
$url .= sprintf('%s?$s',
$_SERVER['PHP_SELF'], $_SERVER['QUERY_STRING']);
return $url;
}
// Parse path from given url or the current url
static function url_path(?string $url = null, bool $htmlentities = true) {
$path = parse_url($url ?: self::url(), PHP_URL_PATH);
return ($htmlentities && $path)
? htmlentities($path) : $path;
}
// Parse query string from current url
static function query_string(?string $url = null) {
return parse_url($url ?: self::url(), PHP_URL_QUERY);
}
// Refresh url
static function refresh_url(array $queryFilter = []) {
$url = self::url();
$refresh_url = self::url_path($url);
if (($qs=self::query_string($url)))
$refresh_url .= sprintf('?%s',
Format::http_query_string($qs, $queryFilter));
return $refresh_url;
}
}
?>

View File

@@ -182,7 +182,7 @@ class Internationalization {
global $thisstaff, $thisclient;
$langs = self::availableLanguages();
$lang = strtolower($lang);
$lang = str_replace('-', '_', strtolower($lang));
if (isset($langs[$lang])) {
$info = &$langs[$lang];
if (!isset($info['desc'])) {
@@ -213,7 +213,7 @@ class Internationalization {
static function getLanguageInfo($lang) {
$langs = self::availableLanguages();
return @$langs[strtolower($lang)] ?: array();
return @$langs[str_replace('-', '_', strtolower($lang))] ?: array();
}
static function availableLanguages($base=I18N_DIR) {
@@ -251,7 +251,7 @@ class Internationalization {
static function isLanguageInstalled($code) {
$langs = self::availableLanguages();
return isset($langs[strtolower($code)]);
return isset($langs[strtolower((string) $code)]);
}
static function isLanguageEnabled($code) {

View File

@@ -63,7 +63,7 @@ class CsvImporter {
$named_fields[$f->get('name')] = $f;
// Read the first row and see if it is a header or not
if (!($data = fgetcsv($this->stream, 1000, ",")))
if (!($data = fgetcsv($this->stream, 1000, ",", "\"", "")))
throw new ImportError(__('Whoops. Perhaps you meant to send some CSV records'));
$headers = array();
@@ -170,7 +170,7 @@ implements Iterator {
function next() {
do {
if (($csv = fgetcsv($this->stream, 4096, ",")) === false) {
if (($csv = fgetcsv($this->stream, 4096, ",", "\"", "")) === false) {
// Read error
$this->current = false;
break;
@@ -189,7 +189,7 @@ implements Iterator {
foreach ($this->headers as $h => $label) {
$f = $this->fields[$h];
$f->_errors = array();
$T = $f->parse($csv[$i]);
$T = $f->parse(trim($csv[$i]));
if ($f->validateEntry($T) && $f->errors())
throw new ImportDataError(sprintf(__(
/* 1 will be a field label, and 2 will be error messages */

View File

@@ -21,10 +21,15 @@ namespace osTicket\Mail {
use Laminas\Mime\Message as MimeMessage;
use Laminas\Mime\Mime;
use Laminas\Mime\Part as MimePart;
use Laminas\Mail\Header;
use osTicket\Mail\Header\ReturnPath;
use osTicket\Mime\Rfc2231Part;
class Message extends MailMessage {
// Message Id (mid)
private $mid;
// MimeMessage Parts
private $mimeMessage = null;
private $mimeParts = null;
// MimeMessage Content
private $mimeContent = null;
// Default Charset
@@ -53,11 +58,18 @@ namespace osTicket\Mail {
return ($this->hasAttachments() || $this->hasInlineImages());
}
public function getMimeMessageParts() {
if (!isset($this->mimeMessage))
$this->mimeMessage = new MimeMessage();
public function getId() {
if (!isset($this->mid)
&& ($header=$this->getHeader('message-id')))
$this->mid = $header->getId();
return $this->mid;
}
return $this->mimeMessage;
public function getMimeMessageParts() {
if (!isset($this->mimeParts))
$this->mimeParts = new MimeMessage();
return $this->mimeParts;
}
public function getMimeMessageContent() {
@@ -67,8 +79,15 @@ namespace osTicket\Mail {
return $this->mimeContent;
}
public function addHeader($key, $value) {
return $this->getHeaders()->addHeaderLine($key, $value);
public function getHeader(string $name) {
return $this->getHeaders()->getHeader($name);
}
public function addHeader($header, $value=null) {
if (isset($value))
$this->getHeaders()->addHeaderLine($header, $value);
else
$this->getHeaders()->addHeader($header);
}
public function addHeaders(array $headers) {
@@ -88,7 +107,7 @@ namespace osTicket\Mail {
$part = new MimePart($text);
$part->type = Mime::TYPE_TEXT;
$part->charset = $this->charset;
$part->encoding = $encoding ?: Mime::ENCODING_BASE64;
$part->encoding = $encoding;
$this->addMimeContent($part);
}
@@ -102,33 +121,126 @@ namespace osTicket\Mail {
}
public function addInlineImage($id, $file) {
$f = new MimePart($file->getData());
$name = $file->getName();
$asciiName = self::asciiFallback($name);
$f = new Rfc2231Part($file->getData());
$f->id = $id;
$f->type = sprintf('%s; name="%s"',
$file->getMimeType(),
$file->getName());
$f->filename = $file->getName();
$f->disposition = Mime::DISPOSITION_INLINE;
$f->type = $file->getMimeType();
$f->encoding = Mime::ENCODING_BASE64;
$f->disposition = Mime::DISPOSITION_INLINE;
$f->filename = $asciiName;
$f->setRawFilename($name);
$this->addMimePart($f);
$this->hasInlineImages = true;
}
public function addAttachment($file, $name=null) {
$f = new MimePart($file->getData());
$name = $name ?: $file->getName();
$asciiName = self::asciiFallback($name);
$f = new Rfc2231Part($file->getData());
$f->type = $file->getMimeType();
$f->filename = $name ?: $file->getName();
$f->disposition = Mime::DISPOSITION_ATTACHMENT;
$f->encoding = Mime::ENCODING_BASE64;
$f->disposition = Mime::DISPOSITION_ATTACHMENT;
$f->filename = $asciiName;
$f->setRawFilename($name);
$this->addMimePart($f);
$this->hasAttachments = true;
}
public function setFrom($emailOrAddressList, $name=null) {
// Expects a valid date e.g date('r')
public function setDate(string $date) {
$d = new Header\Date($date);
// Laminas auto adds Date upstream when any header is added
// We're clearing it here to we back that-date up like it's
// 99 & 2000 ~ Juvenile
$this->getHeaders()->removeHeader('date');
$this->addHeader($d);
}
// Please use this method to set Message-Id otherwise it will be
// utf-8 endcoded and results is an invalid email & bounces
public function setMessageId(string $id) {
try {
$header = new Header\MessageId();
$header->setId($id);
$this->addHeader($header);
$this->mid = $header->getId();
} catch (\Throwable $t) {
// Ignore invalid mid. Upstream will generate one
}
}
public function getMessageId() {
return $this->getId();
}
// Valid email address required or no return "<>" tag
public function setReturnPath($email) {
try {
// Exception is thrown on invalid email address
$header = new ReturnPath();
$header->addAddress($email);
$this->getHeaders()->removeHeader($header->getType());
$this->addHeader($header);
} catch (\Throwable $t) {
// It's not email - perhaps it's a tag?
if (!strcmp($email, '<>'))
$this->addHeader($header->getFieldName(), $email);
// Silently dropping the invalid path
}
}
public function addInReplyTo($inReplyTo) {
if (!is_array($inReplyTo))
$inReplyTo = explode(' ', $inReplyTo);
try {
$header = new Header\InReplyTo();
$header->setIds($inReplyTo); #nolint
$this->addHeader($header);
} catch (\Throwable $t) {
// Mshenzi
}
}
public function addReferences($references) {
if (!is_array($references))
$references = explode(' ', $references);
try {
$header = new Header\References();
$header->setIds($references); #nolint
$this->addHeader($header);
} catch (\Throwable $t) {
// Mshenzi
}
}
// Set Message Sender is useful for SendMail transport, its basically -f
// parameter in the mail() interface
public function setSender($email, $name=null) {
try {
// Exception is thrown on invalid email address
$header = new Header\Sender();
$header->setAddress($email, $name); #nolint
$this->addHeader($header);
} catch (\Throwable $t) {
// Silently ignore invalid email sender defaults to FROM
// addresses
}
}
public function setFrom($email, $name=null) {
// We're resetting the body here when FROM address changes - e.g
// after failed send attempt while trying multiple SMTP accounts
unset($this->body);
return parent::setFrom($emailOrAddressList, $name);
return parent::setFrom($email, $name);
}
// This is used to set FROM & and clear Sender to a new Email Address
public function setOriginator($email, $name=null) {
// Set the FROM Header
$this->setFrom($email, $name);
// Remove Sender Header
$this->getHeaders()->removeHeader('sender');
}
public function setContentType($contentType) {
@@ -179,6 +291,14 @@ namespace osTicket\Mail {
$this->setBody();
}
private static function asciiFallback(string $utf8): string {
$ascii = @iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $utf8);
$ascii = $ascii !== false ? $ascii : '';
// Sanitize to header-safe ASCII
$ascii = preg_replace('/[^A-Za-z0-9._-]+/', '_', $ascii ?? '') ?? '';
return $ascii !== '' ? $ascii : $utf8;
}
}
// This is a wrapper class for Mime/Message that generates multipart
@@ -208,8 +328,8 @@ namespace osTicket\Mail {
// Attempt to connect to the mail server
$connect = $setting->getConnectionConfig();
// Let's go Brandon
parent::connect($connect['host'], $connect['port'],
$connect['ssl']);
parent::__construct($connect['host'], $connect['port'],
$connect['ssl'], true);
// Attempt authentication based on MailBoxAccount settings
$auth = $setting->getAuthCredentials();
switch (true) {
@@ -218,7 +338,9 @@ namespace osTicket\Mail {
throw new Exception('cannot login, user or password wrong');
break;
case $auth instanceof OAuth2AuthCredentials:
if (!$this->oauth2Auth($auth->getAccessToken()))
// Get OAuth2 Authentication Request
$authen = $auth->getAuthRequest($setting->getUser());
if (!$this->oauth2Auth($authen))
throw new Exception('OAuth2 Authentication Error');
break;
default:
@@ -235,7 +357,7 @@ namespace osTicket\Mail {
}
abstract public function __construct($accountSetting);
abstract private function oauth2Auth(AccessToken $token);
abstract protected function oauth2Auth($authen);
}
class ImapMailboxProtocol extends ImapProtocol {
@@ -253,9 +375,8 @@ namespace osTicket\Mail {
* S: A01 (OK|NO|BAD)
* [connection continues...]
*/
private function oauth2Auth(AccessToken $token) {
$this->sendRequest('AUTHENTICATE', ['XOAUTH2',
$token->getAuthRequest()]);
private function oauth2Auth($authen) {
$this->sendRequest('AUTHENTICATE', ['XOAUTH2', $authen]);
while (true) {
$matches = [];
$response = '';
@@ -292,19 +413,19 @@ namespace osTicket\Mail {
* S: (+OK|-ERR|+ {msg})
* [connection continues...]
*/
public function oauth2Auth(AccessToken $token) {
public function oauth2Auth($authen) {
$this->sendRequest('AUTH XOAUTH2');
while (true) {
$response = $this->readLine();
$matches = [];
if ($response == '+') {
// Send xOAuthRequest
$this->sendRequest($token->getAuthRequest());
$this->sendRequest($authen);
} elseif (preg_match("/^\+OK /i", $response)) {
return true;
} elseif (preg_match('/^-ERR (.*+)$/i',
$response, $matches)) {
throw new Exception($matches[2]);
throw new Exception($matches[1]);
} else {
break;
}
@@ -326,6 +447,14 @@ namespace osTicket\Mail {
return trim($result);
}
public function login($user, $password, $tryApop = true) {
try {
parent::login($user, $password, $tryApop);
return true;
} catch (\Throwable $e) {
throw new Exception(__('login failed').': '.$e->getMessage());
}
}
}
// MailBoxStorageTrait
@@ -399,33 +528,27 @@ namespace osTicket\Mail {
*
*/
public function getRawEmail(int $i) {
return $this->getRawHeader($i) . $this->getRawContent($i);
return trim($this->getRawHeader($i)) . "\r\n\r\n" . $this->getRawContent($i);
}
/*
* move an existing message to a folder
*
* Caller should catch possible exception
*/
public function moveMessage($id, $folder) {
try {
return parent::moveMessage($id, $folder);
} catch (\Throwable $t) {
// noop
}
return false;
public function moveMessage($i, $folder) {
parent::moveMessage($i, $folder);
return true;
}
/*
* Remove a message from server.
*
* Caller should catch possible exception
*/
public function removeMessage($i) {
try {
return parent::removeMessage($i);
} catch (\Throwable $t) {
// noop
}
return false;
parent::removeMessage($i);
return true;
}
/*
@@ -461,6 +584,27 @@ namespace osTicket\Mail {
}
}
/**
* Remove a message from server without expunging the mailbox
*
* Laminas Mail (upstream) auto expunges the mailbox on message
* removal or move (copy + remove) - which can cause major issues
* for us since we fetcher uses message sequence numbers to fetch
* messages / emails.
*
* We expunge the mailbox at the end if fetch session.
*
* TODO: Make PR upstream to support calling removeMessage with
* a boolean flag i.e removeMessage(int $id, bool $expunge = true)
*
*/
public function removeMessage($i) {
if (! $this->protocol->store([Storage::FLAG_DELETED], $i, null, '+')) {
throw new Exception('cannot set deleted flag');
}
return true;
}
// Expunge mailbox
public function expunge() {
return $this->protocol->expunge();
@@ -523,21 +667,26 @@ namespace osTicket\Mail {
parent::__construct($this->buildOptions($setting));
}
// Build out SmtpOptions options based on SmtpAccount Settings
private function buildOptions(AccountSetting $setting) {
// Build out SmtpOptions options based on SmtpAccount Settings
$config = [];
// Dont send 'QUIT' on __destruct()
$config = [
'use_complete_quit' => false,
'novalidatecert' => true
];
$connect = $setting->getConnectionConfig();
$auth = $setting->getAuthCredentials();
switch (true) {
case $auth instanceof NoAuthCredentials:
// No Authentication - simply return host and port
return [
'host' => $connect['host'],
'port' => $connect['port']
'host' => $connect['host'],
'port' => $connect['port'],
'name' => $connect['name'],
];
break;
case $auth instanceof BasicAuthCredentials:
$config = [
$config += [
'username' => $auth->getUsername(),
'password' => $auth->getPasswd(),
'ssl' => $connect['ssl'],
@@ -547,7 +696,7 @@ namespace osTicket\Mail {
$token = $auth->getAccessToken();
if ($token->hasExpired())
throw new Exception('Access Token is Expired');
$config = [
$config += [
'xoauth2' => $token->getAuthRequest(),
'ssl' => $connect['ssl'],
];
@@ -557,8 +706,10 @@ namespace osTicket\Mail {
}
return [
'host' => $connect['host'],
'port' => $connect['port'],
'host' => $connect['host'],
'port' => $connect['port'],
'name' => $connect['name'],
'connection_time_limit' => 300, # 5 minutes limit
'connection_class' => $auth->getConnectionClass(),
'connection_config' => $config
];
@@ -694,6 +845,12 @@ namespace osTicket\Mail {
return $this->token;
}
public function getAuthRequest($user=null) {
return $this->getToken()
? $this->getToken()->getAuthRequest($user)
: null;
}
public function getAccessToken($signature=false) {
$token = $this->getToken();
// check signature if requested
@@ -721,31 +878,43 @@ namespace osTicket\Mail {
// We allow scheme to hint for encryption for people using ssl or tls
// on nonstandard ports.
$host = $account->getHost();
$port = (int) $account->getPort();
$ssl = null;
$matches = [];
if (preg_match('~^(ssl|tls)://(.*+)$~iu', $host, $matches))
if (preg_match('~^(ssl|tls|plain)://(.*+)$~iu',
strtolower($host), $matches)) {
list(, $ssl, $host) = $matches;
// Set ssl or tls on based on standard ports
$port = $account->getPort();
if (!$ssl && $port) {
// Clear ssl when "plain" connection is being forced without
// using port number as the indicator!
$ssl = strcmp($ssl, 'plain') ? $ssl : null;
// Why would someone use a standard encryption based port
// for unencrypted connection is beyond me - but apparently
// it's a thing!!
} elseif (!$ssl && $port) {
// Set ssl or tls on based on standard ports
if (in_array($port, [465, 993, 995]))
$ssl = 'ssl';
elseif (in_array($port, [587]))
$ssl = 'tls';
}
// Set the connection settings
$this->connection = [
'host' => $host,
'port' => (int) $port,
'port' => $port,
'ssl' => $ssl,
'protocol' => strtoupper($account->getProtocol()),
'name' => null
'name' => self::get_hostname(),
];
// Set errors to null to clear validation
$this->errors = null;
}
public function getUser() {
return $this->account->getEmail()->getEmail();
}
public function getName() {
return $this->connection['name'];
}
@@ -798,7 +967,7 @@ namespace osTicket\Mail {
public function describe() {
return sprintf('%s://%s:%s/%s',
$this->getSsl() ?: 'none',
$this->getSsl() ?: 'plain',
$this->getHost(),
$this->getPort(),
$this->getProtocol());
@@ -807,15 +976,17 @@ namespace osTicket\Mail {
private function validate() {
if (!isset($this->errors)) {
// Set errors to an array to to make sure don't
// unneccesarily validate valid info again.
$this->errors = [];
// We're simply making sure required info are set. True
// validation will happen at the protocol connection level
$info = $this->getConnectionConfig();
foreach (['host', 'port', 'protocol'] as $p ) {
if (!isset($info[$p]) || !$info[$p])
$this->errors[$p] = sprintf('%s %s',
strtoupper($p), __('Required'));
}
// TODO: Validate hostname - for now we're punting to be
// validated at the protocol connection level
}
return !count($this->errors);
}
@@ -827,6 +998,29 @@ namespace osTicket\Mail {
public function getErrors() {
return $this->errors;
}
/*
* get_hostname
*
* Please note that this is different from getHost above
*
* Here we're getting the hostname to use on HELO/EHLO when
* initiating an SMTP connection. It should be a valid hostname with
* valid reverse-lookup for better deliverability.
*
* Perhaps this can be a setting in the future but allowing users
* to set it to **anything** will results in more mail issues than just
* defaulting to what the OS tells us or localhost for that matter.
*
* For now, we're simply asking core osTicket to give us the OS hostname
* otherwise it will default to localhost which some mail servers frawns
* upon since it won't have a valid reverse-lookup.
*
*/
private static function get_hostname() {
// We're simply returning what the OS is telling us!
return php_uname('n');
}
}
}
@@ -878,3 +1072,27 @@ namespace osTicket\Mail\Protocol\Smtp\Auth {
}
}
}
namespace osTicket\Mail\Header {
use Laminas\Mail\Header\AbstractAddressList;
use Laminas\Mail\Header\HeaderInterface;
use Laminas\Mail\Address;
class ReturnPath extends AbstractAddressList {
protected $fieldName = 'Return-Path';
protected static $type = 'return-path';
public function addAddress($email) {
$this->getAddressList()->add(new Address($email)); #nolint
}
public function getFieldValue($format = HeaderInterface::FORMAT_RAW) {
// We're simply intercepting Value here to add <> to the email
return sprintf('<%s>', parent::getFieldValue($format));
}
public function getType() {
return self::$type;
}
}
}

View File

@@ -20,33 +20,33 @@ namespace osTicket\Mail;
class Mailer {
private $from = null;
var $email = null;
var $accounts = [];
var $smtpAccounts = [];
var $ht = array();
var $attachments = array();
var $options = array();
var $eol="\n";
function __construct(\Email $email=null, array $options=array()) {
function __construct(?\Email $email=null, array $options=array()) {
global $cfg;
// Get all possible outgoing emails accounts (SMTP) to try
if (($email instanceof \Email)
&& ($smtp=$email->getSmtpAccount(false))
&& $smtp->isActive()) {
$this->accounts[$smtp->getId()] = $smtp;
$this->smtpAccounts[$smtp->getId()] = $smtp;
}
if ($cfg) {
// Get Default MTA (SMTP)
if (($smtp=$cfg->getDefaultMTA()) && $smtp->isActive()) {
$this->accounts[$smtp->getId()] = $smtp;
$this->smtpAccounts[$smtp->getId()] = $smtp;
// If email is not set then use Default MTA
if (!$email)
$email = $smtp->getEmail();
} elseif (!$email && $cfg && ($email=$cfg->getDefaultEmail())) {
// as last resort we will send via Default Email
if (($smtp=$email->getSmtpAccount(false)) && $smtp->isActive())
$this->accounts[$smtp->getId()] = $smtp;
$this->smtpAccounts[$smtp->getId()] = $smtp;
}
}
@@ -64,7 +64,7 @@ class Mailer {
}
function getSmtpAccounts() {
return $this->accounts;
return $this->smtpAccounts;
}
function getEmail() {
@@ -77,7 +77,7 @@ class Mailer {
$this->from = $from;
elseif (\Validator::is_email($from)) {
$this->from = new \EmailAddress(
sprintf('"%s" <%s>', $name ?: '', $from));
str_contains($from, '<') ? $from : sprintf('"%s" <%s>', $name ?: '', $from));
} elseif (is_string($from))
$this->from = new \EmailAddress($from);
elseif (($email=$this->getEmail())) {
@@ -110,19 +110,15 @@ class Mailer {
}
function addAttachment(\Attachment $attachment) {
// XXX: This looks too assuming; however, the attachment processor
// in the ::send() method seems hard coded to expect this format
$this->attachments[] = $attachment;
$this->attachments[$attachment->getFile()->getUId()] = $attachment;
}
function addAttachmentFile(\AttachmentFile $file) {
// XXX: This looks too assuming; however, the attachment processor
// in the ::send() method seems hard coded to expect this format
$this->attachments[] = $file;
$this->attachments[$file->getUId()] = $file;
}
function addFileObject(\FileObject $file) {
$this->attachments[] = $file;
$this->attachments[$file->getUId()] = $file;
}
function addAttachments($attachments) {
@@ -136,6 +132,19 @@ class Mailer {
}
}
/**
* lookup Attached File by Key
*/
function getFile(String $key) {
foreach ($this->getAttachments() as $uid => $F) {
if ($F instanceof \Attachment)
$F = $F->getFile();
if (strcasecmp($F->getKey(), $key) === 0)
return $F;
}
return \AttachmentFile::lookup($key);
}
/**
* getMessageId
*
@@ -337,20 +346,19 @@ class Mailer {
$messageId = $this->getMessageId($recipients, $options);
$subject = preg_replace("/(\r\n|\r|\n)/s",'', trim($subject));
$from = $this->getFromAddress($options);
// Create new ostTicket/Mail/Message object
$message = new Message();
// Set our custom Message-Id
$message->setMessageId($messageId);
// Set From Address
$from = $this->getFromAddress($options);
$message->setFrom($from->getEmail(), $from->getName());
// Set Subject
$message->setSubject($subject);
$headers = array (
'Date'=> date('D, d M Y H:i:s O'),
'Message-ID' => "<{$messageId}>",
'X-Mailer' =>'osTicket Mailer',
);
// Collect Generic Headers
$headers = ['X-Mailer' =>'osTicket Mailer'];
// Add in the options passed to the constructor
$options = ($options ?: array()) + $this->options;
@@ -400,10 +408,9 @@ class Mailer {
// Return-Path
if (isset($options['nobounce']) && $options['nobounce'])
$headers['Return-Path'] = '<>';
$message->setReturnPath('<>');
elseif ($this->getEmail() instanceof \Email)
$headers['Return-Path'] = sprintf('<%s>',
$this->getEmail()->getEmail());
$message->setReturnPath($this->getEmail()->getEmail());
// Bulk.
if (isset($options['bulk']) && $options['bulk'])
@@ -425,16 +432,11 @@ class Mailer {
'Auto-Submitted' => 'auto-generated');
// In-Reply-To
if (isset($options['inreplyto']) && $options['inreplyto'])
$headers += array('In-Reply-To' => $options['inreplyto']);
$message->addInReplyTo($options['inreplyto']);
// References
if (isset($options['references']) && $options['references']) {
if (is_array($options['references']))
$headers += array('References' =>
implode(' ', $options['references']));
else
$headers += array('References' => $options['references']);
}
if (isset($options['references']) && $options['references'])
$message->addReferences($options['references']);
// Add Headers
$message->addHeaders($headers);
@@ -536,24 +538,13 @@ class Mailer {
$self = $this;
$body = preg_replace_callback('/cid:([\w.-]{32})/',
function($match) use ($domain, $message, $self) {
$file = false;
foreach ($self->attachments as $id=>$F) {
if ($F instanceof \Attachment)
$F = $F->getFile();
if (strcasecmp($F->getKey(), $match[1]) === 0) {
$file = $F;
break;
}
}
if (!$file)
// Not attached yet attempt to attach it inline
$file = \AttachmentFile::lookup($match[1]);
if (!$file)
if (!($file=$self->getFile($match[1])))
return $match[0];
try {
$message->addInlineImage($match[1].$domain, $file);
// Don't re-attach the image below
unset($self->attachments[$file->getId()]);
unset($self->attachments[$file->getUId()]);
return $match[0].$domain;
} catch(\Exception $ex) {
$self->logWarning(sprintf("%1\$s:%2\$s\n\n%3\$s\n",
@@ -568,16 +559,12 @@ class Mailer {
//XXX: Attachments
if(($attachments=$this->getAttachments())) {
foreach($attachments as $file) {
// Read the filename from the Attachment if possible
if ($file instanceof \Attachment) {
$filename = $file->getFilename();
$file = $file->getFile();
} elseif ($file instanceof \AttachmentFile) {
} else {
$filename = $file->getName();
} elseif ($file instanceof \FileObject) {
$filename = $file->getFilename();
} else
continue;
}
try {
$message->addAttachment($file, $filename);
@@ -592,46 +579,60 @@ class Mailer {
// Try possible SMTP Accounts - connections are cached per request
// at the account level.
foreach ($this->getSmtpAccounts() ?: [] as $account) {
foreach ($this->getSmtpAccounts() ?: [] as $smtpAccount) {
try {
// Overwrite FROM Address if the account doesn't allow spoofing
if (!$account->allowSpoofing())
$message->setFrom(
// Check if SPOOFING is allowed.
if (!$smtpAccount->allowSpoofing()
&& strcasecmp($smtpAccount->getEmail()->getEmail(),
$this->getFromEmail())) {
// If the Account DOES NOT allow spoofing then set the
// Sender as the smtp account sending out the email
// TODO: Allow Aliases to Spoof parent account by
// default.
$message->setSender(
// get Account Email
(string) $account->getEmail()->getEmail(),
(string) $smtpAccount->getEmail()->getEmail(),
// Try to keep the name if available
$this->getFromName() ?: $account->getName());
if (($smtp=$account->getSmtpConnection())
$this->getFromName() ?: $smtpAccount->getName() ?: $this->getEmail());
}
// Attempt to send the Message.
if (($smtp=$smtpAccount->getSmtpConnection())
&& $smtp->sendMessage($message))
return $messageId;
return $message->getId();
} catch (\Exception $ex) {
// Log the SMTP error
$this->logError(sprintf("%1\$s: %2\$s (%3\$s)\n\n%4\$s\n",
_S("Unable to email via SMTP"),
$account->getEmail()->getEmail(),
$account->getHostInfo(),
$smtpAccount->getEmail()->getEmail(),
$smtpAccount->getHostInfo(),
$ex->getMessage()
));
}
// Reset FROM address
$message->setFrom($this->getFromEmail(), $this->getFromName());
// Attempt Failed: Reset FROM to original email and clear Sender
$message->setOriginator($this->getFromEmail(), $this->getFromName());
}
//No SMTP or it failed....use php's native mail function.
$args = [];
if (isset($options['from_address']))
$args[] = '-f '.$options['from_address'];
elseif (($from=$this->getFromAddress()))
$args = ['-f '.$from->getEmail()];
// No SMTP or it FAILED....use Sendmail transport (PHP mail())
// Set Sender / Originator
if (isset($options['from_address'])) {
// This is often set via Mailer::sendmail()
$message->setSender($options['from_address']);
} elseif (($from=$this->getFromAddress())) {
// This should be already set but we're making doubly sure
$message->setOriginator($from->getEmail(), $from->getName());
}
try {
// ostTicket/Mail/Sendmail transport
// ostTicket/Mail/Sendmail transport (mail())
// Set extra params as needed
// NOTE: Laminas Mail doesn't respect -f param set Sender (above)
$args = [];
$sendmail = new Sendmail($args);
if ($sendmail->sendMessage($message))
return $messageId;
return $message->getId();
} catch (\Exception $ex) {
$this->logError(sprintf("%1\$s\n\n%2\$s\n",
_S("Unable to email via php mail function"),
_S("Unable to email via Sendmail"),
$ex->getMessage()
));
}

View File

@@ -24,7 +24,7 @@ class Fetcher {
function __construct(\MailboxAccount $account, $charset='UTF-8') {
$this->account = $account;
$this->mbox = $account->getMailBox();
if ($folder = $this->getFolder())
if (($folder = $this->getFetchFolder()))
$this->mbox->selectFolder($folder);
}
@@ -44,8 +44,8 @@ class Fetcher {
return $this->account->getMaxFetch();
}
function getFolder() {
return $this->account->getFolder();
function getFetchFolder() {
return $this->account->getFetchFolder();
}
function getArchiveFolder() {
@@ -56,9 +56,17 @@ class Fetcher {
return $this->account->canDeleteEmails();
}
function getTicketsApi() {
// We're forcing CLI interface - this is absolutely necessary since
// Email Fetching is considered a CLI operation regardless of how
// it's triggered (cron job / task or autocron)
// Please note that PHP_SAPI cannot be trusted for installations
// using php-fpm or php-cgi binaries for php CLI executable.
if (!isset($this->api))
$this->api = new \TicketApiController();
$this->api = new \TicketApiController('cli');
return $this->api;
}
@@ -67,70 +75,129 @@ class Fetcher {
return ($this->mbox && $this->mbox->noop());
}
function createTicket(int $i) {
function processMessage(int $i, array $defaults = []) {
try {
// Please note that the returned object could be anything from
// ticket, task to thread entry or a boolean.
// Don't let TicketApi call fool you!
return $this->getTicketsApi()->processEmail(
$this->mbox->getRawEmail($i));
$this->mbox->getRawEmail($i), $defaults);
} catch (\TicketDenied $ex) {
// If a ticket is denied we're going to report it as processed
// so it can be moved out of the inbox or deleted.
// so it can be moved out of the Fetch Folder or Deleted based
// on the MailBox settings.
return true;
} catch (\EmailParseError $ex) {
// Log the parse error + headers as a warning
$this->log(sprintf("%s\n\n%s",
// Upstream we try to create a ticket on email parse error - if
// it fails then that means we have invalid headers.
// For Debug purposes log the parse error + headers as a warning
$this->logWarning(sprintf("%s\n\n%s",
$ex->getMessage(),
$this->mbox->getRawHeader($i)));
} catch (\Throwable $t) {
//noop
}
return false;
}
function processEmails() {
// We need a connection
if(!$this->mbox)
if (!$this->mbox)
return false;
// Get basic fetch settings
$archiveFolder = $this->getArchiveFolder();
$delete = $this->canDeleteEmails();
$max = $this->getMaxFetch();
// Get full message count
$messageCount = $this->mbox->countMessages();
$deleteFetched = $this->canDeleteEmails();
$max = $this->getMaxFetch() ?: 30; // default to 30 if not set
// Get message count in the Fetch Folder
if (!($messageCount = $this->mbox->countMessages()))
return 0;
// If the number of emails in the folder are more than Max Fetch
// then process the latest $max emails - this is necessary when
// fetched emails are not getting archived or deleted, which might
// lead to fetcher being stuck 4ever processing old emails already
// fetched
if ($messageCount > $max) {
// Latest $max messages
$messages = range($messageCount-$max, $messageCount);
} else {
// Create a range of message sequence numbers (msgno) to fetch
// starting from the oldest taking max fetch into account
$messages = range(1, min($max, $messageCount));
}
$defaults = [
'emailId' => $this->getEmailId()
];
$msgs = $errors = 0;
for($i = $messageCount; $i > 0; $i--) { // Process messages in reverse.
// Okay, let's create the ticket now
if ($this->createTicket($i)) {
// Mark the message as "Seen" (IMAP only)
$this->mbox->markAsSeen($i);
// Attempt to move the message else attempt to delete
if((!$archiveFolder || !$this->mbox->moveMessage($i, $archiveFolder)) && $delete)
$this->mbox->removeMessage($i);
$msgs++;
$errors = 0; // We are only interested in consecutive errors.
} else {
$errors++;
// TODO: Use message UIDs instead of ids
foreach ($messages as $i) {
try {
// Okay, let's try to create a ticket
if (($result=$this->processMessage($i, $defaults))) {
// Mark the message as "Seen" (IMAP only)
$this->mbox->markAsSeen($i);
// Attempt to move the message if archive folder is set or
if ($archiveFolder)
$this->mbox->moveMessage($i, $archiveFolder);
elseif ($deleteFetched) // else delete if deletion is desired
$this->mbox->removeMessage($i);
$msgs++;
$errors = 0; // We are only interested in consecutive errors.
} else {
$errors++;
}
} catch (\Throwable $t) {
// If we have result then exception happened after email
// processing and shouldn't count as an error
if (!$result)
$errors++;
// log the exception as a debug message
$this->logDebug($t->getMessage());
}
if($max && ($msgs>=$max || $errors>($max*0.8)))
break;
}
$this->mbox->expunge();
// Warn on excessive errors
// Expunge the mailbox
$this->mbox->expunge();
// Warn on excessive errors - when errors are more than email
// processed successfully.
if ($errors > $msgs) {
$warn = sprintf(_S('Excessive errors processing emails for %1$s (%2$s). Please manually check the inbox.'),
$this->mbox->getHostInfo(), $this->getEmail());
$this->log($warn);
$warn = sprintf("%s\n\n%s [%d/%d - %d/%d]",
// Mailbox Info
sprintf(_S('Excessive errors processing emails for %1$s (%2$s).'),
$this->mbox->getHostInfo(), $this->getEmail()),
// Fetch Folder
sprintf('%s (%s)',
_S('Please manually check the Fetch Folder'),
$this->getFetchFolder()),
// Counts - sort of cryptic but useful once we document
// what it means
$messageCount, $max, $msgs, $errors);
$this->logWarning($warn);
}
return $msgs;
}
function log($error) {
private function logDebug($msg) {
$this->log($msg, LOG_DEBUG);
}
private function logWarning($msg) {
$this->log($msg, LOG_WARN);
}
private function log($msg, $level = LOG_WARN) {
global $ost;
$ost->logWarning(_S('Mail Fetcher'), $error);
$subj = _S('Mail Fetcher');
switch ($level) {
case LOG_WARN:
$ost->logWarning($subj, $msg);
break;
case LOG_DEBUG:
default:
$ost->logDebug($subj, $msg);
}
}
/*
@@ -174,18 +241,25 @@ class Fetcher {
//Start time
$start_time = \Misc::micro_time();
foreach ($mailboxes as $mailbox) {
//Break if we're 80% into max execution time
// Check if the mailbox is active 4realz by getting credentials
if (!$mailbox->isActive())
continue;
// Break if we're 80% into max execution time
if ((\Misc::micro_time()-$start_time) > ($max_time*0.80))
break;
// Try fetching emails
try {
$mailbox->fetchEmails();
} catch (\Throwable $t) {
if ($mailbox->getNumErrors() >= $MAXERRORS && $ost) {
//We've reached the MAX consecutive errors...will attempt logins at delayed intervals
// XXX: Translate me
$msg="\nosTicket is having trouble fetching emails from the following mail account: \n".
$msg = sprintf("\n %s:\n",
_S('osTicket is having trouble fetching emails from the following mail account')).
"\n"._S('Email').": ".$mailbox->getEmail()->getAddress().
"\n"._S('Host Info').": ".$mailbox->getHostInfo();
"\n"._S('Host Info').": ".$mailbox->getHostInfo().
"\n"._S('Error').": ".$t->getMessage().
"\n\n ".sprintf(_S('%1$d consecutive errors. Maximum of %2$d allowed'),
$mailbox->getNumErrors(), $MAXERRORS).

View File

@@ -27,6 +27,7 @@ class Mail_Parse {
var $decode_bodies;
var $struct;
var $headers;
var $charset ='UTF-8'; //Default charset.
@@ -35,18 +36,15 @@ class Mail_Parse {
function __construct(&$mimeMessage, $charset=null){
$this->mime_message = &$mimeMessage;
if($charset)
$this->charset = $charset;
$this->include_bodies = true;
$this->decode_headers = false;
$this->decode_bodies = true;
//Desired charset
if($charset)
if ($charset)
$this->charset = $charset;
$this->splitBodyHeader();
$this->notes = array();
}
@@ -61,14 +59,13 @@ class Mail_Parse {
$info = array('raw' => &$this->mime_message);
Signal::send('mail.received', $this, $info);
$this->splitBodyHeader();
$decoder = new Mail_mimeDecode($this->mime_message);
$this->struct = $decoder->decode($params);
if (PEAR::isError($this->struct))
return false;
$this->headers = &$this->struct->headers;
$info = array(
'raw_header' => &$this->header,
'headers' => &$this->struct->headers,
@@ -132,14 +129,35 @@ class Mail_Parse {
return (count($this->struct->headers) > 1);
}
function getMimeMessage() {
return $this->mime_message;
}
function getMimeFile() {
$file = [
// Use subject if available otherwise use mid.
'name' => sprintf('%s.eml',
$this->getSubject() ?: $this->getMessageId()),
'type' => 'message/rfc822',
'data' => $this->getMimeMessage(),
];
// See if we have transfer encoding
if (($encoding=$this->getHeaderEntry('content-transfer-encoding')))
$file['encoding'] = $encoding;
return $file;
}
function splitBodyHeader() {
$match = array();
if (preg_match("/^(.*?)\r?\n\r?\n./s",
$this->mime_message,
$match)) {
if (!$this->header
&& preg_match("/^(.*?)\r?\n\r?\n./s",
$this->mime_message,
$match)) {
$this->header=$match[1];
}
}
/**
* Takes the header section of an email message with the form of
* Header: Value
@@ -159,7 +177,7 @@ class Mail_Parse {
if (in_array($headers[$i][0], array(" ", "\t"))) {
# Continuation from previous header (runon to next line)
$j=$i-1; while (!isset($headers[$j]) && $j>0) $j--;
$headers[$j] .= " ".ltrim($headers[$i]);
$headers[strtolower($j)] .= " ".ltrim($headers[$i]);
unset($headers[$i]);
} elseif (strlen($headers[$i]) == 0) {
unset($headers[$i]);
@@ -169,6 +187,7 @@ class Mail_Parse {
foreach ($headers as $hdr) {
list($name, $val) = explode(": ", $hdr, 2);
# Create list of values if header is specified more than once
$name = strtolower($name);
if (isset($array[$name]) && $as_array) {
if (is_array($array[$name])) $array[$name][] = $val;
else $array[$name] = array($array[$name], $val);
@@ -183,10 +202,15 @@ class Mail_Parse {
static function findHeaderEntry($headers, $name, $allEntries=false) {
if (!is_array($headers))
$headers = self::splitHeaders($headers, $allEntries);
foreach ($headers as $key=>$val)
if (strcasecmp($key, $name) === 0)
return $val;
return false;
$name = strtolower($name);
return $headers[$name] ?? false;
}
function getHeaderEntry($name) {
return $this->hasHeaders()
? self::findHeaderEntry($this->getHeaders(), $name)
: false;
}
function getStruct(){
@@ -194,13 +218,26 @@ class Mail_Parse {
}
function getHeader() {
if(!$this->header) $this->splitBodyHeader();
if (!$this->header)
$this->splitBodyHeader();
return $this->header;
}
function getHeaders() {
if (!isset($this->headers))
$this->headers = self::splitHeaders($this->getHeader());
return $this->headers;
}
function hasHeaders() {
return ($this->getHeaders());
}
function getError(){
return PEAR::isError($this->struct)?$this->struct->getMessage():'';
return PEAR::isError($this->struct)
? $this->struct->getMessage() : '';
}
@@ -208,68 +245,82 @@ class Mail_Parse {
if (!($header = $this->struct->headers['from']))
return null;
if (is_string($header) && (strpos($header, '<') !== false) && (strpos($header, '>') === false))
$header = $header.'>';
return Mail_Parse::parseAddressList($header, $this->charset);
}
function getDeliveredToAddressList() {
if (!($header = $this->struct->headers['delivered-to']))
return null;
return Mail_Parse::parseAddressList($header, $this->charset);
return self::parseAddressList($this->getHeaderEntry('delivered-to'),
$this->charset);
}
function getToAddressList(){
// Delivered-to incase it was a BBC mail.
$tolist = array();
if ($header = $this->struct->headers['to'])
$tolist = array_merge($tolist,
Mail_Parse::parseAddressList($header, $this->charset));
return $tolist ? $tolist : null;
return self::parseAddressList($this->getHeaderEntry('to'),
$this->charset);
}
function getCcAddressList(){
if (!($header = @$this->struct->headers['cc']))
return null;
return Mail_Parse::parseAddressList($header, $this->charset);
return self::parseAddressList($this->getHeaderEntry('cc'),
$this->charset);
}
function getBccAddressList(){
if (!($header = @$this->struct->headers['bcc']))
return null;
return Mail_Parse::parseAddressList($header, $this->charset);
return self::parseAddressList($this->getHeaderEntry('bcc'),
$this->charset);
}
function getMessageId(){
if (($mid = $this->struct->headers['message-id']) && is_array($mid))
$mid = array_pop(array_filter($mid));
if (!$mid)
if (!($mid = $this->getHeaderEntry('message-id')))
$mid = sprintf('<%s@local>', md5($this->getHeader()));
if ($mid && is_array($mid))
$mid = array_pop(array_filter($mid));
return $mid;
}
function getSubject(){
return Format::mimedecode($this->struct->headers['subject'], $this->charset);
return Format::mimedecode($this->getHeaderEntry('subject'), $this->charset);
}
function getReplyTo() {
if (!($header = @$this->struct->headers['reply-to']))
return null;
return self::parseAddressList($this->getHeaderEntry('reply-to'),
$this->charset);
}
return Mail_Parse::parseAddressList($header, $this->charset);
function getInReplyTo() {
return $this->getHeaderEntry('in-reply-to');
}
function getReferences() {
return $this->getHeaderEntry('references');
}
function getRecipients() {
$recipients = [];
if (($to = $this->getToAddressList()))
$recipients['to'] = $to;
if (($cc = $this->getCcAddressList()))
$recipients['cc'] = $cc;
if (($dt = $this->getDeliveredToAddressList()))
$recipients['delivered-to'] = $dt;
return $recipients;
}
function isBounceNotice() {
if (!($body = $this->getPart($this->struct, 'message/delivery-status')))
if (!$this->struct
|| !($body = $this->getPart($this->struct,
'message/delivery-status')))
return false;
$info = self::splitHeaders($body);
if (!isset($info['Action']))
if (!isset($info['action']))
return false;
return strcasecmp($info['Action'], 'failed') === 0;
return strcasecmp($info['action'], 'failed') === 0;
}
function getDeliveryStatusMessage() {
@@ -292,7 +343,7 @@ class Mail_Parse {
// Handle rfc1892 style bounces
if (strtolower($ctype) === 'text/rfc822-headers') {
$body = $p->body . "\n\nIgnored";
$T = new Mail_mimeDecode($body);
$T = new Mail_mimeDecode($body, ['attachOnParseError' => false]);
if ($struct = $T->decode())
return $struct->headers;
}
@@ -303,6 +354,11 @@ class Mail_Parse {
function getBody(){
global $cfg;
// When struct is not set then it might mean decode error - return
// empty body
if (!$this->struct)
return new TextThreadEntryBody('');
if ($cfg && $cfg->isRichTextEnabled()) {
if ($html=$this->getPart($this->struct,'text/html'))
$body = new HtmlThreadEntryBody($html);
@@ -318,8 +374,7 @@ class Mail_Parse {
if (!isset($body))
$body = new TextThreadEntryBody('');
if ($cfg && $cfg->stripQuotedReply())
elseif ($cfg && $cfg->stripQuotedReply())
$body->stripQuotedReply($cfg->getReplySeparator());
return $body;
@@ -443,6 +498,11 @@ class Mail_Parse {
&& $part->headers['content-disposition']
&& preg_match('/filename="([^"]+)"/', $part->headers['content-disposition'], $matches)) {
$filename = Format::mimedecode($matches[1], $this->charset);
} elseif (preg_match('/filename\*\d+=([^;]+);/', $part->headers['content-disposition']) === 1) {
$filename = '';
foreach ($part->d_parameters as $key=>$fname)
if (strpos($key, 'filename*') !== false)
$filename .= Format::decodeRfc5987($fname);
} else {
// Not an attachment?
return false;
@@ -490,8 +550,12 @@ class Mail_Parse {
return $files;
}
if($part==null)
if($part==null) {
$part=$this->getStruct();
if (!$part->parts)
if($result=$this->getAttachments($part))
$files=array_merge($files,$result);
}
if($part->parts){
foreach($part->parts as $k=>$p){
@@ -510,7 +574,7 @@ class Mail_Parse {
// http://msdn.microsoft.com/en-us/library/ee237166(v=exchg.80).aspx
$urgency = 4 - $this->tnef->Importance;
}
elseif ($priority = Mail_Parse::parsePriority($this->getHeader())) {
elseif ($priority = self::parsePriority($this->getHeader())) {
$urgency = $priority + 1;
}
if ($urgency) {
@@ -522,6 +586,114 @@ class Mail_Parse {
}
}
function getHeaderInfo() {
if (!$this->hasHeaders())
return null;
$info = [];
$info['emailId'] = 0;
$info['recipients'] = array();
$info['subject'] = $this->getSubject();
$info['header'] = $this->getHeader();
$info['mid'] = $this->getMessageId();
$info['priorityId'] = $this->getPriority();
$info['mailflags'] = new ArrayObject();
//FROM address: who sent the email.
if (($fromlist = $this->getFromAddressList())) {
$from=$fromlist[0]; //Default.
foreach ($fromlist as $fromobj) {
if(!Validator::is_email($fromobj->mailbox.'@'.$fromobj->host)) continue;
$from = $fromobj;
break;
}
$info['email'] = $from->mailbox.'@'.$from->host;
$info['name'] = trim($from->personal,'"');
if($from->comment && $from->comment[0])
$info['name'].= ' ('.$from->comment[0].')';
//Use email address as name when FROM address doesn't have a name.
if(!$info['name'] && $info['email'])
$info['name'] = $info['email'];
}
/* Scan through the list of addressees (via To, Cc, and Delivered-To headers), and identify
* how the mail arrived at the system. One of the mails should be in the system email list.
* The recipient list (without the Delivered-To addressees) will be made available to the
* ticket filtering system. However, addresses in the Delivered-To header should never be
* considered for the collaborator list.
*/
$recipients = $this->getRecipients();
$info['system_emails'] = $info['thread_entry_recipients'] = [];
foreach ($recipients as $source => $list) {
foreach($list as $addr) {
if (!($emailId=Email::getIdByEmail(strtolower($addr->mailbox).'@'.$addr->host))) {
//Skip virtual Delivered-To addresses
if ($source == 'delivered-to') continue;
$name = trim(@$addr->personal, '"');
$email = strtolower($addr->mailbox).'@'.$addr->host;
$info['recipients'][] = array(
'source' => sprintf(_S("Email (%s)"), $source),
'name' => $name,
'email' => $email);
$info['thread_entry_recipients'][$source][] = sprintf('%s <%s>', $name, $email);
} elseif ($emailId) {
$info['system_emails'][] = $emailId;
$system_email = Email::lookup($emailId);
$info['thread_entry_recipients']['to'][] = (string) $system_email;
if (!$info['emailId'])
$info['emailId'] = $emailId;
}
}
}
$info['thread_entry_recipients']['to'] = isset($info['thread_entry_recipients']['to'])
? array_unique($info['thread_entry_recipients']['to'])
: [];
/*
* In the event that the mail was delivered to the system although none of the system
* mail addresses are in the addressee lists, be careful not to include the addressee
* in the collaborator list. Therefore, the delivered-to addressees should be flagged so they
* are not added to the collaborator list in the ticket creation process.
*/
if (isset($recipients['delivered-to'])) {
foreach ($recipients['delivered-to'] as $addr) {
foreach ($info['recipients'] as $i=>$r) {
if (strcasecmp($r['email'], $addr->mailbox.'@'.$addr->host) === 0)
$info['recipients'][$i]['source'] = 'delivered-to';
}
}
}
//maybe we got BCC'ed??
if(isset($recipients['bcc'])) {
foreach ($recipients['bcc'] as $addr) {
if (($emailId=Email::getIdByEmail($addr->mailbox.'@'.$addr->host))) {
$info['system_emails'][] = $emailId;
if (!$info['emailId'])
$info['emailId'] = $emailId;
}
}
}
// reply to ?
if (($replyto = $this->getReplyTo())
&& ($replyto = $replyto[0])) {
$info['reply-to'] = $replyto->mailbox.'@'.$replyto->host;
if ($replyto->personal)
$info['reply-to-name'] = trim($replyto->personal, " \t\n\r\0\x0B\x22");
}
$info['system_emails'] = array_unique($info['system_emails']);
$info['to-email-id'] = $info['emailId'];
$info['in-reply-to'] = $this->getInReplyTo();
$info['references'] = $this->getReferences();
$info['mailflags']['bounce'] = TicketFilter::isBounce($info['header']);
return $info;
}
/**
* Return a normalized priority urgency from the email headers received.
*
@@ -597,11 +769,16 @@ class Mail_Parse {
}
class EmailDataParser {
var $stream;
var $error;
private $options = [
'attachOnParseError' => true
];
private $stream;
private $errors = [];
function __construct($stream=null) {
function __construct($stream=null, array $options = []) {
$this->stream = $stream;
if ($options)
$this->options = array_merge($this->options, $options);
}
function parse($stream) {
@@ -611,156 +788,68 @@ class EmailDataParser {
if(is_resource($stream)) {
while(!feof($stream))
$contents .= fread($stream, 8192);
} else {
$contents = $stream;
}
$parser= new Mail_Parse($contents);
if(!$parser->decode()) //Decode...returns false on decoding errors
return $this->err('Email parse failed ['.$parser->getError().']');
if (!$parser->decode())
return $this->onParseError($parser);
$data =array();
$data['emailId'] = 0;
$data['recipients'] = array();
$data['subject'] = $parser->getSubject();
$data['header'] = $parser->getHeader();
$data['mid'] = $parser->getMessageId();
$data['priorityId'] = $parser->getPriority();
$data['mailflags'] = new ArrayObject();
//FROM address: who sent the email.
if(($fromlist = $parser->getFromAddressList())) {
$from=$fromlist[0]; //Default.
foreach($fromlist as $fromobj) {
if(!Validator::is_email($fromobj->mailbox.'@'.$fromobj->host)) continue;
$from = $fromobj;
break;
}
$data['email'] = $from->mailbox.'@'.$from->host;
$data['name'] = trim($from->personal,'"');
if($from->comment && $from->comment[0])
$data['name'].= ' ('.$from->comment[0].')';
//Use email address as name when FROM address doesn't have a name.
if(!$data['name'] && $data['email'])
$data['name'] = $data['email'];
}
/* Scan through the list of addressees (via To, Cc, and Delivered-To headers), and identify
* how the mail arrived at the system. One of the mails should be in the system email list.
* The recipient list (without the Delivered-To addressees) will be made available to the
* ticket filtering system. However, addresses in the Delivered-To header should never be
* considered for the collaborator list.
*/
$tolist = array();
if (($to = $parser->getToAddressList()))
$tolist['to'] = $to;
if (($cc = $parser->getCcAddressList()))
$tolist['cc'] = $cc;
if (($dt = $parser->getDeliveredToAddressList()))
$tolist['delivered-to'] = $dt;
$data['system_emails'] = array();
$data['thread_entry_recipients'] = array();
foreach ($tolist as $source => $list) {
foreach($list as $addr) {
if (!($emailId=Email::getIdByEmail(strtolower($addr->mailbox).'@'.$addr->host))) {
//Skip virtual Delivered-To addresses
if ($source == 'delivered-to') continue;
$name = trim(@$addr->personal, '"');
$email = strtolower($addr->mailbox).'@'.$addr->host;
$data['recipients'][] = array(
'source' => sprintf(_S("Email (%s)"), $source),
'name' => $name,
'email' => $email);
$data['thread_entry_recipients'][$source][] = sprintf('%s <%s>', $name, $email);
} elseif ($emailId) {
$data['system_emails'][] = $emailId;
$system_email = Email::lookup($emailId);
$data['thread_entry_recipients']['to'][] = (string) $system_email;
if (!$data['emailId'])
$data['emailId'] = $emailId;
}
}
}
$data['thread_entry_recipients']['to'] = isset($data['thread_entry_recipients']['to'])
? array_unique($data['thread_entry_recipients']['to'])
: [];
/*
* In the event that the mail was delivered to the system although none of the system
* mail addresses are in the addressee lists, be careful not to include the addressee
* in the collaborator list. Therefore, the delivered-to addressees should be flagged so they
* are not added to the collaborator list in the ticket creation process.
*/
if ($tolist['delivered-to']) {
foreach ($tolist['delivered-to'] as $addr) {
foreach ($data['recipients'] as $i=>$r) {
if (strcasecmp($r['email'], $addr->mailbox.'@'.$addr->host) === 0)
$data['recipients'][$i]['source'] = 'delivered-to';
}
}
}
//maybe we got BCC'ed??
if($bcc = $parser->getBccAddressList()) {
foreach ($bcc as $addr) {
if (($emailId=Email::getIdByEmail($addr->mailbox.'@'.$addr->host))) {
$data['system_emails'][] = $emailId;
if (!$data['emailId'])
$data['emailId'] = $emailId;
}
}
}
$data['system_emails'] = array_unique($data['system_emails']);
if (!($data = $parser->getHeaderInfo()))
return false;
if ($parser->isBounceNotice()) {
// Fetch the original References and assign to 'references'
if ($headers = $parser->getOriginalMessageHeaders()) {
if (($headers = $parser->getOriginalMessageHeaders())) {
$data['references'] = $headers['references'];
$data['in-reply-to'] = $headers['message-id'] ?: @$headers['in-reply-to'] ?: null;
$data['in-reply-to'] = $headers['message-id']
?: ($data['in-reply-to'] ?: null);
}
// Fetch deliver status report
$data['message'] = $parser->getDeliveryStatusMessage() ?: $parser->getBody();
$data['thread-type'] = 'N';
$data['mailflags']['bounce'] = true;
}
else {
} else {
// Typical email
$data['message'] = $parser->getBody();
$data['in-reply-to'] = @$parser->struct->headers['in-reply-to'];
$data['references'] = @$parser->struct->headers['references'];
$data['mailflags']['bounce'] = TicketFilter::isBounce($data['header']);
}
$data['to-email-id'] = $data['emailId'];
if (($replyto = $parser->getReplyTo())) {
$replyto = $replyto[0];
$data['reply-to'] = $replyto->mailbox.'@'.$replyto->host;
if ($replyto->personal)
$data['reply-to-name'] = trim($replyto->personal, " \t\n\r\0\x0B\x22");
}
$data['attachments'] = $parser->getAttachments();
return $data;
}
function err($error) {
$this->error = $error;
private function attachOnParseError() {
return (bool) $this->options['attachOnParseError'];
}
return false;
private function onParseError($parser) {
$error = sprintf('%s: [%s]',
__('Email parse failed'),
$parser->getError());
if ($this->attachOnParseError()
&& $parser
// Make sure we can get header info
&& ($data=$parser->getHeaderInfo())) {
// Text thread entry body with the warning / error
$data['message'] = new TextThreadEntryBody($error);
// Add Mime message as an Attachment
$data['attachments'][] = $parser->getMimeFile();
return $data;
}
return $this->err($error);
}
private function err($error) {
// add error and return false
return ($this->addError($error) && false);
}
function getErrors() {
return $this->errors;
}
function addError($error) {
$this->errors [] = $error;
}
function getError() {
@@ -768,7 +857,7 @@ class EmailDataParser {
}
function lastError() {
return $this->error;
return end($this->getErrors());
}
}
?>

View File

@@ -183,7 +183,7 @@ abstract class BaseMessageStorage implements MessageStorageBackend {
return $messages;
}
function getIterator() {
function getIterator(): Traversable {
$this->used = true;
$messages = $this->load();
if ($this->queued) {

48
include/class.mime.php Normal file
View File

@@ -0,0 +1,48 @@
<?php
/**
* class.mime.php
*
* osTicket MIME utilities — extensions for Laminas\Mime components.
*
* @author JediKev <kevin@enhancesoft.com>
* @copyright Copyright (c) osTicket <gpl@osticket.com>
*
*/
namespace osTicket\Mime {
use Laminas\Mime\Mime;
use Laminas\Mime\Part;
class Rfc2231Part extends Part {
private ?string $rawfilename = null;
/**
* Store the raw (UTF-8) filename for RFC-2231 emission.
*/
public function setRawFilename(string $name): void {
$this->rawfilename = $name;
}
/**
* Override to append ; filename*=UTF-8''<percent-encoded> to Content-Disposition.
* Keep everything else from the parent as-is.
*/
public function getHeadersArray($eol = Mime::LINEEND): array {
$headers = parent::getHeadersArray($eol);
if ($this->rawfilename === null || $this->rawfilename === '')
return $headers;
// RFC 2231: UTF-8 then percent-encode (spaces as %20)
$encoded = rawurlencode($this->rawfilename);
foreach ($headers as &$h) {
if (strcasecmp($h[0], 'Content-Disposition') === 0) {
$h[1] .= "; filename*=UTF-8''{$encoded}";
break;
}
}
unset($h);
return $headers;
}
}
}

View File

@@ -226,29 +226,6 @@ class Misc {
return (object) array('start' => $start, 'end' => $end);
}
//Current page
static function currentURL() {
$str = 'http';
if (isset($_SERVER['HTTPS']) && $_SERVER['HTTPS'] == 'on') {
$str .='s';
}
$str .= '://';
if (!isset($_SERVER['REQUEST_URI'])) { //IIS???
$_SERVER['REQUEST_URI'] = substr($_SERVER['PHP_SELF'],1 );
if (isset($_SERVER['QUERY_STRING'])) {
$_SERVER['REQUEST_URI'].='?'.$_SERVER['QUERY_STRING'];
}
}
if ($_SERVER['SERVER_PORT']!=80) {
$str .= $_SERVER['SERVER_NAME'].':'.$_SERVER['SERVER_PORT'].$_SERVER['REQUEST_URI'];
} else {
$str .= $_SERVER['SERVER_NAME'].$_SERVER['REQUEST_URI'];
}
return $str;
}
static function realpath($path) {
$rp = realpath($path);
return $rp ? $rp : $path;

View File

@@ -23,6 +23,7 @@ namespace osTicket\OAuth2 {
protected $expires;
protected $refreshToken;
protected $resourceOwnerId;
protected $scope;
// osTicket specific
protected $resourceOwnerEmail;
protected $configSignature;
@@ -49,6 +50,9 @@ namespace osTicket\OAuth2 {
if (!empty($options['resource_owner_email']))
$this->resourceOwnerEmail = $options['resource_owner_email'];
if (!empty($options['scope']))
$this->scope = $options['scope'];
}
public function getToken() {
@@ -79,6 +83,10 @@ namespace osTicket\OAuth2 {
return $this->getResourceOwnerEmail();
}
public function getScope() {
return $this->scope;
}
public function getConfigSignature() {
return $this->configSignature;
}
@@ -95,14 +103,17 @@ namespace osTicket\OAuth2 {
return $this->hasExpired();
}
public function getAuthRequest() {
public function isMatch($email, $strict=false) {
return (!$strict || strcasecmp($this->getResourceOwnerEmail(), $email) === 0);
}
public function getAuthRequest($user=null) {
if ($this->hasExpired())
throw new Exception('Access Token is Expired');
return base64_encode(sprintf("user=%s\1auth=Bearer %s\1\1",
$this->getResourceOwner(),
$this->getAccessToken()
));
$user ?? $this->getResourceOwner(),
$this->getAccessToken()));
}
public function __toString() {

View File

@@ -233,16 +233,16 @@ class ModelMeta implements ArrayAccess {
return $root;
}
function offsetGet($field) {
function offsetGet($field): mixed {
return $this->meta[$field];
}
function offsetSet($field, $what) {
function offsetSet($field, $what): void {
$this->meta[$field] = $what;
}
function offsetExists($field) {
function offsetExists($field): bool {
return isset($this->meta[$field]);
}
function offsetUnset($field) {
function offsetUnset($field): void {
throw new Exception('Model MetaData is immutable');
}
@@ -1368,7 +1368,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
return $list[0];
}
function count() {
function count(): int {
// Defer to the iterator if fetching already started
if (isset($this->_iterator)) {
return $this->_iterator->count();
@@ -1562,7 +1562,7 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
}
// IteratorAggregate interface
function getIterator($iterator=false) {
function getIterator($iterator=false): Traversable {
if (!isset($this->_iterator)) {
$class = $iterator ?: $this->getIteratorClass();
$it = new $class($this);
@@ -1593,16 +1593,16 @@ class QuerySet implements IteratorAggregate, ArrayAccess, Serializable, Countabl
}
// ArrayAccess interface
function offsetExists($offset) {
function offsetExists($offset): bool {
return $this->getIterator()->offsetExists($offset);
}
function offsetGet($offset) {
function offsetGet($offset): mixed {
return $this->getIterator()->offsetGet($offset);
}
function offsetUnset($a) {
function offsetUnset($a): void {
throw new Exception(__('QuerySet is read-only'));
}
function offsetSet($a, $b) {
function offsetSet($a, $b): void {
throw new Exception(__('QuerySet is read-only'));
}
@@ -1735,27 +1735,27 @@ implements ArrayAccess {
$this->inner->rewind();
}
function getIterator() {
function getIterator(): Traversable {
$this->asArray();
return new ArrayIterator($this->storage);
}
function offsetExists($offset) {
function offsetExists($offset): bool {
$this->fillTo($offset+1);
return count($this->storage) > $offset;
}
function offsetGet($offset) {
function offsetGet($offset): mixed {
$this->fillTo($offset+1);
return $this->storage[$offset];
}
function offsetUnset($a) {
function offsetUnset($a): void {
throw new Exception(__('QuerySet is read-only'));
}
function offsetSet($a, $b) {
function offsetSet($a, $b): void {
throw new Exception(__('QuerySet is read-only'));
}
function count($mode=COUNT_NORMAL) {
function count($mode=COUNT_NORMAL): int {
$this->asArray();
return count($this->storage);
}
@@ -1999,7 +1999,7 @@ implements IteratorAggregate {
return $model;
}
function getIterator() {
function getIterator(): Traversable {
$func = ($this->map) ? 'getRow' : 'getArray';
$func = array($this->resource, $func);
$cache = true;
@@ -2028,27 +2028,27 @@ implements Iterator {
$this->callback = $callback;
}
function rewind() {
function rewind(): void {
$this->eoi = false;
$this->next();
}
function key() {
function key(): mixed {
return $this->key;
}
function valid() {
function valid(): bool {
if (!isset($this->eoi))
$this->rewind();
return !$this->eoi;
}
function current() {
function current(): mixed {
if ($this->eoi) return false;
return $this->current;
}
function next() {
function next(): void {
try {
$cbk = $this->callback;
$this->current = $cbk();
@@ -2073,7 +2073,7 @@ implements IteratorAggregate {
$this->queryset = $queryset;
}
function getIterator() {
function getIterator(): Traversable {
$this->resource = $this->queryset->getQuery();
return new CallbackSimpleIterator(function() {
global $StopIteration;
@@ -2096,7 +2096,7 @@ implements IteratorAggregate {
$this->queryset = $queryset;
}
function getIterator() {
function getIterator(): Traversable {
$this->resource = $this->queryset->getQuery();
return new CallbackSimpleIterator(function() {
global $StopIteration;
@@ -2171,7 +2171,7 @@ extends ModelResultSet {
* Slight edit to the standard iteration method which will skip deleted
* items.
*/
function getIterator() {
function getIterator(): Traversable {
return new CallbackFilterIterator(parent::getIterator(),
function($i) { return !$i->__deleted__; }
);
@@ -2244,11 +2244,11 @@ extends ModelResultSet {
return clone $this->queryset;
}
function offsetUnset($a) {
function offsetUnset($a): void {
$this->fillTo($a);
$this->storage[$a]->delete();
}
function offsetSet($a, $b) {
function offsetSet($a, $b): void {
$this->fillTo($a);
if ($obj = $this->storage[$a])
$obj->delete();
@@ -2637,6 +2637,12 @@ class SqlCompiler {
function getJoins($queryset) {
$sql = '';
// Move CDATA to top of joins
if (array_key_exists("cdata", $this->joins)) {
$temp = ['cdata' => $this->joins['cdata']];
unset($this->joins['cdata']);
$this->joins = $temp + $this->joins;
}
foreach ($this->joins as $path => $j) {
if (!isset($j['sql']))
continue;
@@ -2871,7 +2877,7 @@ class MySqlCompiler extends SqlCompiler {
$table = ($rmeta['view'])
// XXX: Support parameters from the nested query
? $rmodel::getSqlAddParams($this)
: $this->quote($rmeta['table']);
: $this->quote($rmeta['table'], true);
$base = "{$join}{$table} {$alias}";
return array($base, $constraints);
}

View File

@@ -28,7 +28,6 @@ use Symfony\Component\ClassLoader\UniversalClassLoader_osTicket;
define('LOG_WARN',LOG_WARNING);
class osTicket {
var $loglevel=array(1=>'Error','Warning','Debug');
//Page errors.
@@ -36,10 +35,6 @@ class osTicket {
//System
var $system;
var $warning;
var $message;
@@ -59,9 +54,11 @@ class osTicket {
require_once(INCLUDE_DIR.'class.company.php');
// Load the config
$this->config = new OsticketConfig();
// Start session (if not disabled)
if (!defined('DISABLE_SESSION') || !DISABLE_SESSION)
$this->session = osTicketSession::start(SESSION_TTL,
// Start Session
if (!defined('SESSION_SESSID'))
define('SESSION_SESSID', 'OSTSESSID');
$this->session = osTicketSession::start(SESSION_SESSID, SESSION_TTL,
$this->isUpgradePending());
// CSRF Token
$this->csrf = new CSRF('__CSRFToken__');
@@ -240,7 +237,8 @@ class osTicket {
if($email) {
$email->sendAlert($to, $subject, $message, null, array('text'=>true, 'reply-tag'=>false));
} else {//no luck - try the system mail.
Mailer::sendmail($to, $subject, $message, '"'.__('osTicket Alerts').sprintf('" <%s>',$to));
osTicket\Mail\Mailer::sendmail($to, $subject, $message,
'"'.__('osTicket Alerts').sprintf('" <%s>',$to));
}
//log the alert? Watch out for loops here.
@@ -322,7 +320,8 @@ class osTicket {
.',title='.db_input(Format::sanitize($title, true))
.',log_type='.db_input($loglevel[$level])
.',log='.db_input(Format::sanitize($message, false))
.',ip_address='.db_input($_SERVER['REMOTE_ADDR']);
.',ip_address='.db_input($_SERVER['REMOTE_ADDR'])
.',logger=""';
db_query($sql, false);
@@ -368,10 +367,10 @@ class osTicket {
static function get_path_info() {
if(isset($_SERVER['PATH_INFO']))
return $_SERVER['PATH_INFO'];
return htmlentities($_SERVER['PATH_INFO']);
if(isset($_SERVER['ORIG_PATH_INFO']))
return $_SERVER['ORIG_PATH_INFO'];
return htmlentities($_SERVER['ORIG_PATH_INFO']);
//TODO: conruct possible path info.
@@ -535,6 +534,25 @@ class osTicket {
$_SERVER['HTTP_HOST'] . ROOT_PATH);
}
/*
* get_client_port
*
* Get client PORT from "Http_X-Forwarded-PORT" if we have trusted
* proxies set.
* FIXME: Follow trusted proxies chain
*
*/
static function get_client_port($header='HTTP_X_FORWARDED_PORT') {
$port = $_SERVER['SERVER_PORT'];
// We're just making sure we have Trusted Proxies
// FIXME: Validate
$proxies = self::getTrustedProxies();
if (isset($_SERVER[$header]) && $proxies)
$port = $_SERVER[$header];
return $port;
}
/*
* get_client_ip
*
@@ -587,7 +605,7 @@ class osTicket {
if (!$proxies)
return false;
// Wildcard set - trust all proxies
else if ($proxies == '*')
else if (in_array('*', $proxies))
return true;
return ($proxies && Validator::check_ip($ip, $proxies));

View File

@@ -2,10 +2,10 @@
/*********************************************************************
class.ostsession.php
Custom osTicket session handler.
osTicket Session Management Backend
Peter Rotich <peter@osticket.com>
Copyright (c) 2006-2013 osTicket
Copyright (c) 2022 osTicket
http://www.osticket.com
Released under the GNU General Public License WITHOUT ANY WARRANTY.
@@ -13,85 +13,259 @@
vim: expandtab sw=4 ts=4 sts=4:
**********************************************************************/
include_once INCLUDE_DIR.'class.session.php';
class osTicketSession {
static $backends = array(
'db' => 'DbSessionBackend',
'memcache' => 'MemcacheSessionBackend',
'system' => 'FallbackSessionBackend',
);
// Session SSID
private $name = 'OSTSESSID';
// Session Backend instance
private $backend;
// Session default TTL
private $ttl;
var $ttl = SESSION_TTL;
var $data = '';
var $data_hash = '';
var $id = '';
var $backend;
function __construct($ttl=0, $checkdbversion=false){
$this->ttl = $ttl ?: ini_get('session.gc_maxlifetime') ?: SESSION_TTL;
// Set osTicket specific session name.
session_name('OSTSESSID');
// Forced cleanup on shutdown
register_shutdown_function('session_write_close');
// Set session cleanup time to match TTL
ini_set('session.gc_maxlifetime', $ttl);
// Skip db version check if version is later than 1.7
if ((!defined('MAJOR_VERSION') || $checkdbversion)
&& OsticketConfig::getDBVersion())
return session_start();
# Cookies
// Avoid setting a cookie domain without a dot, thanks
// http://stackoverflow.com/a/1188145
$domain = null;
if (isset($_SERVER['HTTP_HOST'])
&& strpos($_SERVER['HTTP_HOST'], '.') !== false
&& !Validator::is_ip($_SERVER['HTTP_HOST']))
// Remote port specification, as it will make an invalid domain
list($domain) = explode(':', $_SERVER['HTTP_HOST']);
session_set_cookie_params($ttl, ROOT_PATH, $domain,
function __construct($name, $ttl = SESSION_TTL, $checkdbversion = false) {
// session name/ssid
if ($name && strcmp($this->name, $name))
$this->name = $name;
// Session ttl cannot exceed php.ini maxlifetime setting
$maxlife = ini_get('session.gc_maxlifetime');
$this->ttl = min($ttl ?: ($maxlife ?: SESSION_TTL), $maxlife);
// Set osTicket specific session name/sessid
session_name($this->name);
// Set Default cookie Params before we start the session
session_set_cookie_params($this->ttl, ROOT_PATH, Http::domain(),
osTicket::is_https(), true);
if (!defined('SESSION_BACKEND'))
define('SESSION_BACKEND', 'db');
/** Determine Session Backend to use **/
if ((!defined('MAJOR_VERSION') || $checkdbversion)
&& OsticketConfig::getDBVersion())
// Default PHP SessionHandler
$bk = 'system';
else // default is database
$bk = self::session_backend() ?: 'database';
// Tag the backend with storage backend class
switch ($bk) {
case 'database':
$bk = "$bk:DatabaseSessionStorageBackend";
break;
case 'memcache':
$bk = "$bk:MemcacheSessionStorageBackend";
break;
case 'memcache.database':
// memcache is primary while database is secondary
// Database doesn't store data when set as secondary, but
// very useful when session data is offloaded to memcache with
// database tracking sessions ids and expire time for the
// purpose of knowing who is online.
$bk = sprintf('%s:%s:%s',
'memcache',
'MemcacheSessionStorageBackend',
'DatabaseSessionStorageBackend');
break;
case 'database.memcache':
// database is primary while memcache is secondary
// This setup only makes sense if memcache is being used as
// backup backend
$bk = sprintf('%s:%s:%s',
'database',
'DatabaseSessionStorageBackend',
'MemcacheSessionStorageBackend');
break;
case 'system':
case 'noop':
// system & noop don't require storage backends
break;
default:
// Assume invalid entry - default to system
$bk = 'system';
}
/** Session Backend options **/
$options = [
// Default TTL (maxlife)
'session_ttl' => $this->ttl,
// It indicates that the session is API session - which session
// handler should handle as stateless for new sessions.
'api_session' => defined('API_SESSION'),
'callbacks' => $this->getCallbacks($bk),
];
// Set MaxLifeTime if defined. This is defined per user so it's
// preferred over ttl.
if (defined('SESSION_MAXLIFE') && is_numeric(SESSION_MAXLIFE))
$options['session_maxlife'] = SESSION_MAXLIFE;
// If $bk doesn't exit then an Exception will be thrown
// Backend is object is turned on success
try {
$bk = SESSION_BACKEND;
if (!class_exists(self::$backends[$bk]))
$bk = 'db';
$this->backend = new self::$backends[$bk]($this->ttl);
}
catch (Exception $x) {
// Use the database for sessions
trigger_error($x->getMessage(), E_USER_WARNING);
$this->backend = new self::$backends['db']($this->ttl);
// If $bk doesn't exit then an Exception will be thrown
// backend object is turned on success
$this->backend = osTicket\Session\SessionBackend::register($bk,
$options, true);
} catch (Throwable $t) {
die($t->getMessage());
// We're just gonna default to php session handler and hope for
// the best.
// TODO: Log the error and perhaps rethrow the exception so it
// can be fatal?
}
if ($this->backend instanceof SessionBackend) {
// Set handlers.
session_set_save_handler(
array($this->backend, 'open'),
array($this->backend, 'close'),
array($this->backend, 'read'),
array($this->backend, 'write'),
array($this->backend, 'destroy'),
array($this->backend, 'gc')
);
}
// Start the session.
// Finally start the damn session.
session_start();
}
function regenerate_id(){
$oldId = session_id();
session_regenerate_id();
$this->backend->destroy($oldId);
// returns session callbacks we might be interested in monitoring
private function getCallbacks($bk) {
return [
// see onClose routine for details
'close' => [$this, 'onClose']
];
}
// onClose - is used to signal those interested on changing session
// data, to do so, before data is commited.
public function onClose($handler) {
$i = new ArrayObject(['touched' => false]);
Signal::send('session.close', null, $i);
return (bool) $i['touched'];
}
/*
* session_backend
*
* Get configured session backend if any.
*/
static function session_backend($default = false) {
// Session Disabled?
// This is useful when fetching logos or on CLI - we don't want to update the
// session in such cases.
if (defined('NOOP_SESSION') || defined('DISABLE_SESSION'))
return 'noop'; // Ignore session data
// No session backend set - return default
if (!defined('SESSION_BACKEND'))
return $default;
// Explode backend incase it's chained
list($bk, $secondary) = explode('.', SESSION_BACKEND);
// Only recongnize supported primary backends
switch (strtolower($bk)) {
case 'memcache':
// Make sure we have memcache servers defined - if not
// then break so we can return default.
// TODO: Log an error or throw an exception
if (!defined('MEMCACHE_SERVERS'))
break;
// No break on purpose
case 'database':
case 'system':
return strtolower(SESSION_BACKEND);
break;
default:
return $default;
}
}
/*
* registered_backend
*
* get registered backend if any
*/
static function registered_backend() {
global $ost;
if ($ost && isset($ost->session))
return $ost->session->backend;
}
/*
* regenerate($ttl)
*
* Regenerate current session_id and expire the old one in $ttl seconds
* This is preferred over destroying the session immediately just incase
* we have pending ajax requests for example
*
*/
static function regenerate(int $ttl=60) {
// Make sure session is active and headers are not already sent
if (session_status() !== PHP_SESSION_ACTIVE
|| headers_sent())
return false;
// Save current ($old) session id
$old = session_id();
// Expire current session cookie now + ttl
// We have to do it here before re regenerate becase renewCookie has
// hardcoded session calls to get name & id
// FIXME: Maybe bae...
self::renewCookie(time(), $ttl);
// Regenerate the session
// TODO: use session_create_id() instead of session_regenerate_id -
// but PHP (even 8) is still bugy and inconsistent when it comes to
// using SessionIdInterface::create_sid depening on session settings.
session_regenerate_id(false);
// get a new session id and force commit
// Expire old session now + ttl
session_write_close();
// Expire old session now + ttl
// ::expire() is not a standard session routine so we have to commit
// the current "new" session first
self::expire($old, $ttl);
// Restart the new session
session_start();
// Return the new session id
return session_id();
}
/*
* expire session
*
* Expire session in $ttl from now if the storage backend supports
* it otherwise it should destroy it by calling this->destroy($id)
*/
// Expire session - end is near mb!
static function expire($id, int $ttl) {
// See if we have a backend to ask to expire the session - otherwise
// we destroy session now!
if (!($backend=self::registered_backend()))
return false;
// Expire session soonish (now() + $ttl) - end is near mb!
return (bool) $backend->expire($id, $ttl);
}
// Aks the backend to destroy a session now
static function destroy($id) {
// Expire with ttl of 0 destroys the session
return (bool) self::expire($id, 0);
}
// Cleanup Expired Sessions
static function cleanup() {
// get active backend
if (!($backend=self::registered_backend()))
return false;
return (bool) $backend->cleanup();
}
static function renewCookie($baseTime=false, $window=false) {
global $ost;
$ttl = $window ?: SESSION_TTL;
$expire = ($baseTime ?: time()) + $ttl;
$opts = [
'expires' => $expire,
'path' => ini_get('session.cookie_path'),
'domain' => ini_get('session.cookie_domain'),
'secure' => ini_get('session.cookie_secure'),
'httponly' => ini_get('session.cookie_httponly'),
'samesite' => !empty($ost->getConfig()->getAllowIframes()) ? 'None' : 'Strict'
];
setcookie(session_name(), session_id(), $opts);
// Trigger expire update - neeed for secondary handlers that only
// log new sessions
self::expire(session_id(), $ttl);
}
static function destroyCookie() {
@@ -102,252 +276,409 @@ class osTicketSession {
ini_get('session.cookie_httponly'));
}
static function renewCookie($baseTime=false, $window=false) {
setcookie(session_name(), session_id(),
($baseTime ?: time()) + ($window ?: SESSION_TTL),
ini_get('session.cookie_path'),
ini_get('session.cookie_domain'),
ini_get('session.cookie_secure'),
ini_get('session.cookie_httponly'));
}
/* helper functions */
function get_online_users($sec=0){
$sql='SELECT user_id FROM '.SESSION_TABLE.' WHERE user_id>0 AND session_expire>NOW()';
if($sec)
$sql.=" AND TIME_TO_SEC(TIMEDIFF(NOW(),session_updated))<$sec";
$users=array();
if(($res=db_query($sql)) && db_num_rows($res)) {
while(list($uid)=db_fetch_row($res))
$users[] = $uid;
}
static function get_online_users(int $seconds = 0) {
// Authoretative lookup is DatabaseSessionRecords assuming
// database is the primary backend or secondary logger
$records = DatabaseSessionRecord::active_sessions([
'lastseen' => $seconds,
'authenticated' => true,
]);
$users = [];
foreach ($records as $record)
$users[] = $record->getUserId();
return $users;
}
/* ---------- static function ---------- */
static function start($ttl=0, $checkdbversion=false) {
return new static($ttl, $checkdbversion);
static function start($name, $ttl=0, $checkdbversion=false) {
return new static($name, $ttl, $checkdbversion);
}
}
abstract class SessionBackend {
var $isnew = false;
var $ttl;
function __construct($ttl=SESSION_TTL) {
$this->ttl = $ttl;
}
function open($save_path, $session_name) {
return true;
}
function close() {
return true;
}
function getTTL() {
return $this->ttl;
}
function write($id, $data) {
// Last chance session update
$i = new ArrayObject(array('touched' => false));
Signal::send('session.close', null, $i);
return $this->update($id, $i['touched'] ? session_encode() : $data);
}
function cleanup() {
$this->gc(0);
}
abstract function read($id);
abstract function update($id, $data);
abstract function destroy($id);
abstract function gc($maxlife);
}
class SessionData
extends VerySimpleModel {
/*
* DatabaseSessionRecord
*
* ORM hook to session table with SessionRecordInterface implementation
*/
class DatabaseSessionRecord extends VerySimpleModel
implements osTicket\Session\SessionRecordInterface {
static $meta = array(
'table' => SESSION_TABLE,
'pk' => array('session_id'),
);
}
class DbSessionBackend
extends SessionBackend {
var $data = null;
public function isNew() {
return $this->__new__;
}
function read($id) {
public function isValid() {
// NOTE: We're not enforcing IP match here because it's a user space
// setting and checked elsewhere
// User Agent must remain same for the lifecycle of the session
if (isset($this->user_agent)
&& strcmp($_SERVER['HTTP_USER_AGENT'], $this->user_agent) !== 0)
return false;
// Make sure id len is 32
return (strlen($this->getId()) == 32);
}
public function setId(string $id) {
$this->session_id = $id;
return $this;
}
public function getId() {
return $this->session_id;
}
public function setData(?string $data = null) {
$this->session_data = $data;
return $this;
}
public function getData() {
return $this->session_data;
}
public function getExpireTime() {
return $this->session_expire;
}
private function setExpire(int $maxlife) {
$this->session_expire = SqlFunction::NOW()
->plus(SqlInterval::SECOND($maxlife));
}
public function setTTL($ttl) {
$this->setExpire($ttl);
return $this;
}
public function expire(int $ttl) {
$this->setTTL($ttl);
// Assume it will expire shortly - clear user_id
$this->user_id = 0;
return $this->commit();
}
public function getUpdateTime() {
return $this->session_updated;
}
public function commit() {
return ($this->save());
}
public function save($refresh=false) {
global $thisstaff;
// See if we need to set the user id - should only be set once onlogin
if (!$this->user_id && $thisstaff)
$this->user_id = $thisstaff->getId();
if (count($this->dirty))
$this->session_updated = SqlFunction::NOW();
return parent::save($refresh);
}
public function delete() {
return (parent::delete());
}
public function toArray() {
return [
'id' => $this->getId(),
'data' => $this->getData(),
'updated' => $this->getUpdateTime(),
'expires' => $this->getExpireTime(),
];
}
/*
* lookupRecord
*
* Given session id - lookup the record
*
* Possible returns;
* false: on lookup failure
* null: doesn't exists and autocreate is false
* record: fetched or newly created session record
*
*/
public static function lookupRecord($id, $autocreate = false,
$backend=null) {
// We're doing lookup locally so we can auto-create one if the
// session doesn't exist.
$record = false;
try {
$this->data = SessionData::objects()
$record = self::objects()
->filter(['session_id' => $id])
->annotate(array('is_expired' =>
new SqlExpr(new Q(array('session_expire__lt' => SqlFunction::NOW())))))
->one();
if ($this->data->is_expired > 0) {
if ($record->is_expired > 0) {
// session_expire is in the past. Pretend it is expired and
// reset the data. This will assist with CSRF issues
$this->data->session_data='';
$record->session_data = '';
}
$this->id = $id;
}
catch (DoesNotExist $e) {
$this->data = new SessionData(['session_id' => $id, 'session_data' => '']);
// We're auto-creating model (unsaved) when one doesn't exist?
$record = ($autocreate && ctype_alnum($id)) ? self::create($id) : null;
}
catch (OrmException $e) {
return false;
catch (OrmException | Exception $ex) {
// This could happen if more than one record exits in the
// database for example.
}
// Verify the User Agent string
if (isset($this->data->user_agent)
&& (strcmp($_SERVER['HTTP_USER_AGENT'], $this->data->user_agent) !== 0)) {
$this->destroy($id);
return false;
}
return $this->data->session_data;
return $record;
}
function update($id, $data){
global $thisstaff;
static function create($vars) {
// We expect ::init($id) or ::init(array $vars)
if ($vars && !is_array($vars))
$vars = ['session_id' => $vars];
elseif (!isset($vars['session_id']))
// Session Id is required
throw Exception(sprintf('session_id: %s', __('Required')));
if (defined('DISABLE_SESSION') && $this->data->__new__)
return true;
// Set User Session Atrributes
if (!isset($vars['user_ip']))
$vars['user_ip'] = $_SERVER['REMOTE_ADDR'];
if (!isset($vars['user_agent']))
$vars['user_agent'] = $_SERVER['HTTP_USER_AGENT'];
// Default to system SESSION_TTL if session_maxlife is not set
$maxlife = $vars['session_maxlife'] ?? SESSION_TTL;
// filter out the $vars to just the fields we support - backend can
// send way more data than we want.
$vars = array_intersect_key($vars,
array_flip([
'session_id',
'session_data',
'session_expire',
'session_updated',
'user_id',
'user_ip',
'user_agent',
]));
$ttl = $this && method_exists($this, 'getTTL')
? $this->getTTL() : SESSION_TTL;
// Create a session data obj if not loaded.
if (!isset($this->data))
$this->data = new SessionData(['session_id' => $id]);
$this->data->session_data = $data;
$this->data->session_expire =
SqlFunction::NOW()->plus(SqlInterval::SECOND($ttl));
$this->data->user_id = $thisstaff ? $thisstaff->getId() : 0;
$this->data->user_ip = $_SERVER['REMOTE_ADDR'];
$this->data->user_agent = $_SERVER['HTTP_USER_AGENT'];
return $this->data->save();
// Create an instance
$record = new self($vars);
// set session_expire timestamp based on $maxlife
$record->setExpire($maxlife);
// Set updated to now()
$record->session_updated = SqlFunction::NOW();
return $record;
}
function destroy($id){
return SessionData::objects()->filter(['session_id' => $id])->delete() ? true : false;
static function destroy($id) {
return ( (bool) self::objects()
->filter(['session_id' => $id])
->delete());
}
function cleanup() {
$this->gc(0);
}
function gc($maxlife){
SessionData::objects()->filter([
'session_expire__lte' => SqlFunction::NOW()
static function cleanupExpired() {
return self::objects()->filter([
'session_expire__lte' => SqlFunction::NOW()
])->delete();
}
static function user_sessions(int $id) {
$criteria = ['user_id' => $id];
return self::active_sessions($criteria);
}
static function active_sessions(array $criteria = []) {
$criteria['active'] = true;
return self::sessions($criteria);
}
static function expired_sessions(array $criteria = []) {
$criteria['active'] = false;
return self::sessions($criteria);
}
static function sessions(array $criteria = []) {
// now
$now = SqlFunction::NOW();
// empty filters
$filters = [];
// Active or Expired
if (isset($criteria['active']) && $criteria['active']) {
// Active must not be expired
$filters = ['session_expire__gt' => $now];
} elseif (isset($criteria['active'])) {
// expired session if active is set to false
$filters = ['session_expire__lt' => $now];
}
// Authenticated users have user_id set (only Agents at the moment)
if (isset($criteria['user_id']) && $criteria['user_id'])
$filters['user_id'] = $criteria['user_id'];
elseif (isset($criteria['authenticated']) && $criteria['authenticated'])
$filters['user_id__gt'] = 0;
elseif (isset($criteria['authenticated']))
$filters['user_id'] = 0; // Guests only
// last seen since X seconds
if (isset($criteria['lastseen']) && $criteria['lastseen']) {
$interval = new SqlInterval('SECOND', $criteria['lastseen']);
$filters['session_updated__gte'] = $now->minus($interval);
}
return self::objects()->filter($filters);
}
}
class MemcacheSessionBackend
extends SessionBackend {
var $memcache;
var $servers = array();
/*
* Database Session Storage Backend
*
*/
class DatabaseSessionStorageBackend
extends osTicket\Session\AbstractSessionStorageBackend {
function __construct($ttl) {
parent::__construct($ttl);
public function expireRecord($id, $ttl) {
if (!($record=$this->getRecord($id)))
return false;
if (!extension_loaded('memcache'))
throw new Exception('Memcached extension is missing');
if (!defined('MEMCACHE_SERVERS'))
return (bool) $record->expire($ttl);
}
public function saveRecord($record, $secondary=false) {
if ($secondary || !is_a($record, 'DatabaseSessionRecord')) {
// We're only recording new sessions without data if we're
// secondary backend
if ($record->isNew())
$this->writeRecord($record->getId(), null);
return true;
}
return (bool) $record->commit();
}
public function writeRecord($id, $data=null) {
$record = self:: lookupRecord($id, true);
$record->setData($data);
return (bool) $record->commit();
}
public function lookupRecord($id, $autocreate) {
return DatabaseSessionRecord::lookupRecord($id, $autocreate, $this);
}
public function destroyRecord($id) {
return (bool) (DatabaseSessionRecord::destroy($id));
}
public function cleanupExpiredRecords() {
return DatabaseSessionRecord::cleanupExpired();
}
}
/*
* Memchache Session Storage Backend
*
*/
class MemcacheSessionStorageBackend
extends osTicket\Session\AbstractMemcacheSessionStorageBackend {
public function __construct($options) {
// Make sure we have memcache servers
$servers = [];
if (isset($options['servers']) && is_array($options['servers']))
$servers = $options['servers'];
elseif (defined('MEMCACHE_SERVERS'))
$servers = explode(',', MEMCACHE_SERVERS);
// Bro Got Servers or Nah?!
if (!count($servers))
throw new Exception('MEMCACHE_SERVERS must be defined');
$servers = explode(',', MEMCACHE_SERVERS);
$this->memcache = new Memcache();
foreach ($servers as $S) {
@list($host, $port) = explode(':', $S);
if (strpos($host, '/') !== false)
// Use port '0' for unix sockets
$port = 0;
else
$port = $port ?: ini_get('memcache.default_port') ?: 11211;
$this->servers[] = array(trim($host), (int) trim($port));
// FIXME: Crash or warn if invalid $host or $port
}
$options['servers'] = $servers;
parent::__construct($options);
}
function getKey($id) {
return sha1($id.SECRET_SALT);
public function saveRecord($record, $secondary = false) {
if ($secondary || !is_a($record, 'MemcacheSessionRecord')) {
if (!$record->isNew())
return true;
}
return $this->writeRecord($record->getId(), $record->getData());
}
function read($id) {
$key = $this->getKey($id);
// Try distributed read first
foreach ($this->servers as $S) {
list($host, $port) = $S;
$this->memcache->addServer($host, $port);
}
$data = $this->memcache->get($key);
// Read from other servers on failure
if ($data === false && count($this->servers) > 1) {
foreach ($this->servers as $S) {
list($host, $port) = $S;
$this->memcache->pconnect($host, $port);
if ($data = $this->memcache->get($key))
break;
}
}
// No session data on record -- new session
$this->isnew = $data === false;
return $data ?: '';
public function writeRecord($id, $data) {
return $this->set($id, $data);
}
function update($id, $data) {
if (defined('DISABLE_SESSION') && $this->isnew)
return true;
$key = $this->getKey($id);
foreach ($this->servers as $S) {
list($host, $port) = $S;
$this->memcache->pconnect($host, $port);
if (!$this->memcache->replace($key, $data, 0, $this->getTTL()));
$this->memcache->set($key, $data, 0, $this->getTTL());
}
return true;
}
function destroy($id) {
$key = $this->getKey($id);
foreach ($this->servers as $S) {
list($host, $port) = $S;
$this->memcache->pconnect($host, $port);
$this->memcache->replace($key, '', 0, 1);
$this->memcache->delete($key, 0);
}
return true;
}
function gc($maxlife) {
// Memcache does this automatically
public function lookupRecord($id, $autocreate = false) {
if (!($data = $this->get($id)) && !$autocreate)
return false;
return new MemcacheSessionRecord([
'id' => $id,
'new' => $data === false,
'data' => $data,
], $this);
}
}
class FallbackSessionBackend {
// Use default PHP settings, with some edits for best experience
function __construct() {
// FIXME: Consider extra possible security tweaks such as adjusting
// the session.save_path
class MemcacheSessionRecord
implements osTicket\Session\SessionRecordInterface {
private $backend;
private $data;
public function __construct(array $ht, $backend) {
// Make sure we have an id
if (!isset($ht['id']) || !$ht['id'])
throw new Exception('Session id required');
// Set backend
$this->backend = $backend;
// Set the data as array object
$this->data = new ArrayObject($ht, ArrayObject::ARRAY_AS_PROPS);
}
public function isNew() {
return ($this->data->new);
}
public function isValid() {
return (strlen($this->getId()) == 32);
}
public function getId() {
return $this->data->id;
}
public function setId(string $id) {
$this->data->id = $id;
}
public function getData() {
return $this->data->data;
}
public function setData(?string $data = null) {
$this->data->data = $data;
}
public function setTTl(int $ttl) {
$this->data->ttl = $ttl;
}
// Unsupported call but required
public function expire(int $ttl) {
return true;
}
public function commit() {
return $this->backend
? $this->backend->saveRecord($this)
: false;
}
public function toArray() {
return [
'id' => $this->getId(),
'data' => $this->getData(),
];
}
public static function create($vars) {
return new self($vars);
}
}
?>

View File

@@ -26,29 +26,32 @@ class mPDFWithLocalImages extends Mpdf {
$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)) {
if (preg_match_all('/\bsrc\s*=\s*(["\'])\s*cid:\s*([\w._-]{32})\s*\1/i', $html, $cids)) {
foreach (AttachmentFile::objects()
->filter(array('key__in' => $cids[1]))
->filter(array('key__in' => $cids[2]))
as $file
) {
$images[strtolower($file->getKey())] = $file;
}
}
$args[0] = preg_replace_callback('/"cid:([\w.-]{32})"/',
$args[0] = preg_replace_callback('/\bsrc\s*=\s*(["\'])\s*cid:\s*([\w._-]{32})\s*\1/i',
function($match) use ($self, $images, &$filenumber) {
if (!($file = @$images[strtolower($match[1])]))
return $match[0];
if (!($file = @$images[strtolower($match[2])]))
return $match[0]; // leave unchanged if not resolvable
$key = "__attached_file_".$filenumber++;
$self->imageVars[$key] = $file->getData();
return 'var:'.$key;
// Preserve attribute formatting: src="var:..."
return 'src='.$match[1].'var:'.$key.$match[1];
},
$html
);
// unregister phar stream to mitigate vulnerability in mpdf library
@stream_wrapper_unregister('phar');
@stream_wrapper_unregister('php');
call_user_func_array(array('parent', 'WriteHtml'), $args);
// restore phar stream
@stream_wrapper_restore('phar');
@stream_wrapper_restore('php');
}
function output($name = '', $dest = '') {
@@ -74,7 +77,7 @@ class Ticket2PDF extends mPDFWithLocalImages
$this->includenotes = $notes;
$this->includeevents = $events;
parent::__construct(['mode' => 'utf-8', 'format' => $psize, 'tempDir'=>sys_get_temp_dir()]);
parent::__construct(['mode' => 'utf-8', 'format' => $psize, 'tempDir'=>sys_get_temp_dir(), 'autoLangToFont' => true, 'autoScriptToLang' => true]);
$this->_print();
}
@@ -98,7 +101,7 @@ class Ticket2PDF extends mPDFWithLocalImages
return;
$html = ob_get_clean();
$this->autoScriptToLang;
$this->autoScriptToLang = true;
$this->WriteHtml($html, 0, true, true);
}
}
@@ -115,7 +118,7 @@ class Task2PDF extends mPDFWithLocalImages {
$this->task = $task;
$this->options = $options;
parent::__construct(['mode' => 'utf-8', 'format' => $this->options['psize'], 'tempDir'=>sys_get_temp_dir()]);
parent::__construct(['mode' => 'utf-8', 'format' => $this->options['psize'], 'tempDir'=>sys_get_temp_dir(), 'autoLangToFont' => true, 'autoScriptToLang' => true]);
$this->_print();
}
@@ -128,7 +131,7 @@ class Task2PDF extends mPDFWithLocalImages {
ob_start();
include STAFFINC_DIR.'templates/task-print.tmpl.php';
$html = ob_get_clean();
$this->autoScriptToLang;
$this->autoScriptToLang = true;
$this->WriteHtml($html, 0, true, true);
}

View File

@@ -69,7 +69,7 @@ abstract class PluginConfig extends Config {
return $this->instance;
}
function setInstance(PluginInstance $i=null) {
function setInstance(?PluginInstance $i=null) {
$this->instance = $i;
}
@@ -116,7 +116,7 @@ abstract class PluginConfig extends Config {
* returns (true | false)
*
*/
function store(SimpleForm $form = null, &$errors=array()) {
function store(?SimpleForm $form = null, &$errors=array()) {
try {
if ($this->hasCustomConfig())
@@ -368,6 +368,8 @@ class PluginManager {
* registered in the plugin registry -- the %plugin table.
*/
function install($path) {
// Fixup $path to ensure we are only installing from the plugin dir
$path = 'plugins/' . basename($path);
$is_phar = substr($path, strlen($path) - 5) == '.phar';
if (!($info = $this->getInfoForPath(INCLUDE_DIR . $path, $is_phar)))
return false;
@@ -778,7 +780,7 @@ class Plugin extends VerySimpleModel {
return $this->config_class;
}
function getConfig(PluginInstance $instance = null, $defaults = []) {
function getConfig(?PluginInstance $instance = null, $defaults = []) {
if ((!isset($this->config) || $instance)
&& ($class=$this->getConfigClass())) {
// We cache instance config for side loading purposes on

View File

@@ -692,7 +692,7 @@ class CustomQueue extends VerySimpleModel {
foreach (array(
QueueColumn::placeholder(array(
"id" => 1,
"heading" => "Number",
"heading" => __("Number"),
"primary" => 'number',
"width" => 85,
"bits" => QueueColumn::FLAG_SORTABLE,
@@ -702,7 +702,7 @@ class CustomQueue extends VerySimpleModel {
)),
QueueColumn::placeholder(array(
"id" => 2,
"heading" => "Created",
"heading" => __("Created"),
"primary" => 'created',
"filter" => 'date:full',
"truncate" =>'wrap',
@@ -711,7 +711,7 @@ class CustomQueue extends VerySimpleModel {
)),
QueueColumn::placeholder(array(
"id" => 3,
"heading" => "Subject",
"heading" => __("Subject"),
"primary" => 'cdata__subject',
"width" => 250,
"bits" => QueueColumn::FLAG_SORTABLE,
@@ -722,21 +722,21 @@ class CustomQueue extends VerySimpleModel {
)),
QueueColumn::placeholder(array(
"id" => 4,
"heading" => "From",
"heading" => __("From"),
"primary" => 'user__name',
"width" => 150,
"bits" => QueueColumn::FLAG_SORTABLE,
)),
QueueColumn::placeholder(array(
"id" => 5,
"heading" => "Priority",
"heading" => __("Priority"),
"primary" => 'cdata__priority',
"width" => 120,
"bits" => QueueColumn::FLAG_SORTABLE,
)),
QueueColumn::placeholder(array(
"id" => 8,
"heading" => "Assignee",
"heading" => __("Assignee"),
"primary" => 'assignee',
"width" => 100,
"bits" => QueueColumn::FLAG_SORTABLE,
@@ -973,12 +973,13 @@ class CustomQueue extends VerySimpleModel {
// Consider keyword searching
if ($name === ':keywords') {
global $ost;
$qs = $ost->searcher->find($value, $qs, false);
$qs = $ost->searcher->find($value, $qs, false, ['boolean' => false]);
}
else {
$nullable = ($method === 'nset') ? false : null;
// XXX: Move getOrmPath to be more of a utility
// Ensure the special join is created to support custom data joins
$name = @static::getOrmPath($name, $qs);
$name = @static::getOrmPath($name, $qs, $nullable);
if (preg_match('/__answers!\d+__/', $name)) {
$qs->annotate(array($name => SqlAggregate::MAX($name)));
@@ -1466,7 +1467,7 @@ class CustomQueue extends VerySimpleModel {
return $for_parent($pid);
}
static function getOrmPath($name, $query=null) {
static function getOrmPath($name, $query=null, $nullable=null) {
// Special case for custom data `__answers!id__value`. Only add the
// join and constraint on the query the first pass, when the query
// being mangled is received.
@@ -1481,6 +1482,10 @@ class CustomQueue extends VerySimpleModel {
$root = $query->model;
$meta = $root::getMeta()->getByPath($path[1]);
$joins = $meta['joins'];
// If the method is 'nset' (ie. IS NULL) we want to force normal
// JOIN instead of LEFT JOIN to get proper results
if (isset($nullable))
$joins['answers']['null'] = $nullable;
if (!isset($joins[$path[2]])) {
$meta->addJoin($path[2], $joins['answers']);
}
@@ -1593,7 +1598,7 @@ abstract class QueueColumnAnnotation {
}
function getClassName() {
return @$this->config['c'] ?: get_class();
return @$this->config['c'] ?: get_class($this);
}
static function getAnnotations($root) {
@@ -1840,7 +1845,7 @@ extends QueueColumnAnnotation {
function getDecoration($row, $text) {
$flags = $row['flags'];
$linked = ($flags & Ticket::FLAG_LINKED) != 0;
if ($linked && $_REQUEST['a'] == 'search')
if ($linked)
return '<i class="icon-link"></i>';
}

View File

@@ -23,14 +23,21 @@ RolePermission::register(/* @trans */ 'Miscellaneous', ReportModel::getPermissio
class OverviewReport {
var $start;
var $end;
static $end_choices = [
'now' => 'Up to today',
'+7 days' => 'One Week',
'+14 days' => 'Two Weeks',
'+1 month' => 'One Month',
'+3 months' => 'One Quarter'
];
var $format;
function __construct($start, $end='now', $format=null) {
global $cfg;
$this->start = $start;
$this->end = $end;
$this->start = Format::sanitize($start);
$this->end = array_key_exists($end, self::$end_choices) ? $end : 'now';
$this->format = $format ?: $cfg->getDateFormat(true);
}

View File

@@ -156,9 +156,10 @@ class Role extends RoleModel {
}
function update($vars, &$errors) {
if (!$vars['name'])
$name = Format::sanitize($vars['name']);
if (!$name)
$errors['name'] = __('Name required');
elseif (($r=Role::lookup(array('name'=>$vars['name'])))
elseif (($r=Role::lookup(array('name'=>$name)))
&& $r->getId() != $vars['id'])
$errors['name'] = __('Name already in use');
elseif (!$vars['perms'] || !count($vars['perms']))
@@ -167,8 +168,8 @@ class Role extends RoleModel {
if ($errors)
return false;
$this->name = $vars['name'];
$this->notes = $vars['notes'];
$this->name = $name;
$this->notes = Format::sanitize($vars['notes']);
$this->updatePerms($vars['perms'], $errors);
@@ -321,7 +322,7 @@ class RolePermission {
}
function getInfo() {
return $this->perms;
return $this->perms ?: [];
}
function merge($perms) {

View File

@@ -955,7 +955,7 @@ class ScheduleEntry extends VerySimpleModel {
// range at the moment - assuming ends on the same day.
$source['ends_at'] = $ends->format('h:i a');
if (($stops=$this->getStopsDatetime()))
$source['stops_on'] = $stops->setTimestamp();
$source['stops_on'] = $stops->getTimestamp();
// See if time spans all day.
if ($this->isFullDay())
@@ -1049,13 +1049,15 @@ class ScheduleEntry extends VerySimpleModel {
}
static function getDays() {
static $translated = false;
if (!$translated) {
foreach (static::$days as $k=>$v)
static::$days[$k] = __($v);
}
return self::$days;
}
return static::$days;
static function getTranslatedDays() {
static $translated = null;
if (!isset($translated))
$translated = array_map(function ($d) { return __($d); }, static::getDays());
return $translated;
}
static function getWeeks() {
@@ -1176,7 +1178,7 @@ extends AbstractForm {
'default' => "",
'layout' => new GridFluidCell(6),
'label' => __('Day of the Week'),
'choices' => ScheduleEntry::getDays() + array(
'choices' => ScheduleEntry::getTranslatedDays() + array(
'weekdays' => sprintf('%s (%s)',
__('Weekdays'), __('Mon-Fri')),
'weekends' => sprintf('%s (%s)',
@@ -1212,7 +1214,7 @@ extends AbstractForm {
'default' => "",
'layout' => new GridFluidCell(6),
'label' => __('Day'),
'choices' => ScheduleEntry::getDays(),
'choices' => ScheduleEntry::getTranslatedDays(),
'validator-error' => __('Selection required'),
'visibility' => new VisibilityConstraint(
new Q(array('monthly__neq'=>'day')),
@@ -1241,7 +1243,7 @@ extends AbstractForm {
'default' => '',
'layout' => new GridFluidCell(4),
'label' => __('Day'),
'choices' => ScheduleEntry::getDays(),
'choices' => ScheduleEntry::getTranslatedDays(),
'validator-error' => __('Selection required'),
'configuration'=>array('prompt'=>__('Day')),
'visibility' => new VisibilityConstraint(

View File

@@ -44,7 +44,7 @@ abstract class SearchBackend {
);
abstract function update($model, $id, $content, $new=false, $attrs=array());
abstract function find($query, QuerySet $criteria, $addRelevance=true);
abstract function find($query, QuerySet $criteria, $addRelevance=true, $options=array());
static function register($backend=false) {
$backend = $backend ?: get_called_class();
@@ -79,9 +79,9 @@ class SearchInterface {
$this->bootstrap();
}
function find($query, QuerySet $criteria, $addRelevance=true) {
function find($query, QuerySet $criteria, $addRelevance=true, $options=array()) {
$query = Format::searchable($query);
return $this->backend->find($query, $criteria, $addRelevance);
return $this->backend->find($query, $criteria, $addRelevance, $options);
}
function update($model, $id, $content, $new=false, $attrs=array()) {
@@ -327,7 +327,7 @@ class MysqlSearchBackend extends SearchBackend {
return implode('', $results);
}
function find($query, QuerySet $criteria, $addRelevance=true) {
function find($query, QuerySet $criteria, $addRelevance=true, $options=array()) {
global $thisstaff;
// MySQL usually doesn't handle words shorter than three letters
@@ -339,6 +339,10 @@ class MysqlSearchBackend extends SearchBackend {
$mode = ' IN NATURAL LANGUAGE MODE';
// Allow boolean full-text syntax to be disabled
$allow_boolean = !array_key_exists('boolean', $options)
|| $options['boolean'];
// According to the MySQL full text boolean mode, this grammar is
// assumed:
// see http://dev.mysql.com/doc/refman/5.6/en/fulltext-boolean.html
@@ -359,7 +363,8 @@ class MysqlSearchBackend extends SearchBackend {
// Require the use of at least one operator and conform to the
// boolean mode grammar
$T = array();
if (preg_match('`(^|\s)["()<>~+-]`u', $query, $T)
if ($allow_boolean
&& preg_match('`(^|\s)["()<>~+-]`u', $query, $T)
&& preg_match("`^{$BOOLEAN}$`u", $query, $T)
) {
// If using boolean operators, search in boolean mode. This regex
@@ -370,6 +375,13 @@ class MysqlSearchBackend extends SearchBackend {
}
#elseif (count(explode(' ', $query)) == 1)
# $mode = ' WITH QUERY EXPANSION';
// Sanitize query to avoid possible SQL injection via parameter markers
// This regex matches one or more colons followed by one or more digits,
// and then replaces the match with only the digits (i.e. stripping the colon(s)).
$query = preg_replace('/:+(\d+)/', '$1', $query);
// escape query and using it as search
$search = 'MATCH (Z1.title, Z1.content) AGAINST ('.db_input($query).$mode.')';
switch ($criteria->model) {
@@ -467,7 +479,7 @@ class MysqlSearchBackend extends SearchBackend {
* not indexed in the _search table and add it to the index.
*/
function IndexOldStuff() {
$class = get_class();
$class = get_class($this);
$auto_create = function($db_error) use ($class) {
if ($db_error != 1146)
@@ -901,7 +913,10 @@ class SavedQueue extends CustomQueue {
$query = $this->getQuery();
if ($agent)
$query = $agent->applyVisibility($query);
$query->limit(false)->offset(false)->order_by(false);
$query->filter(Q::any([
'ticket_pid__isnull' => true,
'flags__hasbit' => Ticket::FLAG_LINKED
]))->limit(false)->offset(false)->order_by(false);
try {
return $query->count();
} catch (Exception $e) {
@@ -941,6 +956,9 @@ class SavedQueue extends CustomQueue {
->filter(Q::any(array(
'flags__hasbit' => CustomQueue::FLAG_QUEUE,
'staff_id' => $agent->getId(),
)))
->filter(Q::not(array(
'flags__hasbit' => CustomQueue::FLAG_DISABLED,
)));
if ($criteria && is_array($criteria))
@@ -984,7 +1002,13 @@ class SavedQueue extends CustomQueue {
}
if ($Q->constraints && !$empty) {
$expr = SqlCase::N()->when(new SqlExpr(new Q($Q->constraints)), new SqlField('ticket_id'));
$constraints = $Q->constraints;
// Add path_constraints to get the correct counts
foreach ($Q->path_constraints as $pc) {
if (!empty($pc[0]->constraints))
$constraints[] = $pc[0];
}
$expr = SqlCase::N()->when(new SqlExpr(new Q($constraints)), new SqlField('ticket_id'));
$query->aggregate(array(
"q{$queue->id}" => SqlAggregate::COUNT($expr, true)
));
@@ -1151,6 +1175,7 @@ class AdvancedSearchSelectionField extends ChoiceField {
switch ($method) {
case 'includes':
case '!includes':
if (!$value) return;
$Q = new Q();
if (count($value) > 1)
$Q->add(array("{$name}__in" => array_keys($value)));

578
include/class.session.php Normal file
View File

@@ -0,0 +1,578 @@
<?php
/*********************************************************************
class.session.php
osTicket core Session Handlers & Backends
Peter Rotich <peter@osticket.com>
Copyright (c) 2022 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:
**********************************************************************/
namespace osTicket\Session {
// Extend global Exception
class Exception extends \Exception {}
// Session backend factory
// TODO: Allow plugins to register backends.
class SessionBackend {
static private $backends = [
// Database is the default backend for osTicket
'database' => ':AbstractSessionStorageBackend',
// Session data are stored in memcache saving database the pain
// of storing and retriving the records
'memcache' => ':AbstractMemcacheSessionStorageBackend',
// Noop/Null SessionHandler doesn't care about sh*t
'noop' => 'NoopSessionStorageBackend',
// Default SessionHandler
'system' => 'SystemSessionHandler',
];
static public function getBackends() {
return self::$backends;
}
static public function getBackend(string $bk) {
return (($backends=self::getBackends())
&& isset($backends[$bk]))
? $backends[$bk]
: null;
}
static public function has(string $bk) {
return (self::getBackend($bk));
}
static public function factory(string $bk, array $options = []) {
list ($bk, $handler, $secondary) = explode(':', $bk);
if (!($backend=self::getBackend($bk)))
throw new Exception('Unknown Session Backend: '.$bk);
if ($backend[0] == ':') {
// External implementation required
if (!$handler)
throw new Exception(sprintf(
'%s: requires storage backend driver',
$bk));
// External handler needs global namespacing
$handler ="\\$handler";
// handler must implement parent
$impl = substr($backend, 1);
} elseif ($handler) {
$handler ="\\$handler";
} else {
// local handler is being used force this namespace
$handler = sprintf('%s\%s', __NAMESPACE__, $backend);
$impl = ($bk == 'system') ? 'SystemSessionHandler' : 'AbstractSessionHandler';
}
// Make sure handler / backend class exits
if (!$handler
|| !class_exists($handler))
throw new Exception(sprintf('unknown storage backend - [%s]',
$handler));
// Set secondary backend with global namespacing if it is
// chained to primary backend ($bk). Primary backend will
// validate and instanciate it if it can support the interface
// of the said backend.
if ($secondary)
$options['secondary'] = "\\$secondary";
$sessionHandler = new $handler($options);
// Make sure the handler implements the right interface
$impl = sprintf('%s\%s', __NAMESPACE__, $impl);
if ($impl && !is_a($sessionHandler, $impl))
throw new Exception(sprintf('%s: must implement %s',
$handler, $impl));
return $sessionHandler;
}
static public function register(string $bk, array $options = [], bool
$register_shutdown = true) {
if (($handler=self::factory($bk, $options))
&& session_set_save_handler($handler,
$register_shutdown))
return $handler;
}
}
interface SessionRecordInterface {
// Basic setters and getters
function getId();
function setId(string $id);
function getData();
function setData(string $data);
function setTTL(int $ttl);
// expire in ttl & commit
function expire(int $ttl);
// commit / save the record to storage
function commit();
// Checkers
function isNew();
function isValid();
// to array [id, data] expected - other attributes can be returned
// as well dependin on the storage backend
function toArray();
}
abstract class AbstractSessionHandler implements
\SessionHandlerInterface,
\SessionUpdateTimestampHandlerInterface {
// Options
protected $options;
// Callback registry
private $callbacks;
// Flags
private $isnew = false;
private $isapi = false;
// maxlife & ttl
private $ttl;
private $maxlife;
// Secondary backend
protected $secondary = null;
public function __construct($options = []) {
// Set flags based on passed in options
$this->options = $options;
// Default TTL
$this->ttl = $this->options['session_ttl'] ??
ini_get('session.gc_maxlifetime');
// Dynamic maxlife (MLT)
if (isset($this->options['session_maxlife']))
$this->maxlife = $this->options['session_maxlife'];
// API Session Flag
if (isset($this->options['api_session']))
$this->isapi = $this->options['api_session'];
// callbacks
if (isset($options['callbacks'])) {
$this->callbacks = $options['callbacks'];
unset($options['callbacks']);
}
// Set Secondary Backend if any
if (isset($options['secondary']))
$this->setSecondaryBackend($options['secondary']);
}
/*
* set a seconday backend... for now it's private but will be public
* down the road.
*/
private function setSecondaryBackend($backend, $options = null) {
// Ignore invalid backend
if (!$backend
// class exists
|| !class_exists($backend)
// Not same backend as handler
|| !strcasecmp(get_class($this), $backend))
return false;
// Init Secondary handler if set and valid
$options = $options ?? $this->options;
unset($options['secondary']); //unset secondary to avoid loop.
$this->secondary = new $backend($options);
// Make sure it's truly a storage backend and not a Ye!
if (!is_a($this->secondary,
__NAMESPACE__.'\AbstractSessionStorageBackend'))
$this->secondary = null; // nah...
return ($this->secondary);
}
/*
* API Sessions are Stateless and new sessions shouldn't be created
* when this flag is turned on.
*
*/
protected function isApiSession() {
return ($this->isapi);
}
/*
* onEvent
*
* Storage Backends that's interested in monitoring / reporting on
* events can implement this routine.
*
*/
protected function onEvent($event) {
return false;
}
// Handles callback for registered event listeners
protected function callbackOn($event) {
if (!$this->callbacks
|| !isset($this->callbacks[$event]))
return false;
return (($collable=$this->callbacks[$event])
&& is_callable($collable))
? call_user_func($collable, $this)
: false;
}
/*
* pre_save
*
* This is a hook called by session storage backends before saving a
* session record.
*
*/
public function pre_save(SessionRecordInterface $record) {
// We're NOT creating new API Sessions since API is stateless.
// However existing sessions are updated, allowing for External
// Authentication / Authorization to be processed via API endpoints.
if ($record->isNew() && $this->isApiSession())
return false;
return true;
}
/*
* Default osTicket TTL is the default Session's Maxlife
*/
public function getTTL() {
return $this->ttl;
}
/*
* Maxlife is based on logged in user (if any)
*
*/
public function getMaxlife() {
// Prefer maxlife defined based on settings for the
// current session user - otherwise use default ttl
if (!isset($this->maxlife))
$this->maxlife = (defined('SESSION_MAXLIFE')
&& is_numeric(SESSION_MAXLIFE))
? SESSION_MAXLIFE : $this->getTTL();
return $this->maxlife;
}
public function setMaxLife(int $maxlife) {
$this->maxlife = $maxlife;
}
public function open($save_path, $session_name) {
return true;
}
public function close() {
return true;
}
public function write($id, $data) {
// Last chance session update - returns true if update is done
if ($this->onEvent('close'))
$data = session_encode();
return $this->update($id, $data);
}
public function validateId($id) {
return true;
}
public function updateTimestamp($id, $data) {
return true;
}
public function gc($maxlife) {
$this->cleanup();
}
abstract function read($id);
abstract function update($id, $data);
abstract function expire($id, $ttl);
abstract function destroy($id);
abstract function cleanup();
}
abstract class AbstractSessionStorageBackend extends AbstractSessionHandler {
// Record we cache between read & update/write
private $record;
protected function onEvent($event) {
return $this->callbackOn($event);
}
public function getRecord($id, $autocreate = false) {
if (!isset($this->record)
|| !is_object($this->record)
// Mismatch here means new session id
|| strcmp($id, $this->record->getId()))
$this->record = static::lookupRecord($id, $autocreate, $this);
return $this->record;
}
// This is the wrapper for to ask the backend to lookup or
// create/init a record when not found
protected function getRecordOrCreate($id) {
return $this->getRecord($id, true);
}
public function read($id) {
// we're auto creating the record if it doesn't exist so we can
// have a new cached recoed on write/update.
return (($record = $this->getRecordOrCreate($id))
&& !$record->isNew())
? $record->getData()
: '';
}
public function update($id, $data) {
if (!($record = $this->getRecord($id)))
return false;
// Upstream backend can overwrite saving the record via pre_save hook
// depending on the type of session or class of user etc.
if ($this->pre_save($record) === false)
return true; // record is being ignored
// Set id & data
$record->setId($id);
$record->setData($data);
// Ask backend to save the record
if (!$this->saveRecord($record))
return false;
// See if we need to send the record to secondary backend to
// send the record to for logging or audit reasons or whatever!
try {
if (isset($this->secondary))
$this->secondary->saveRecord($record, true);
} catch (\Trowable $t) {
// Ignore any BS!
}
// clear cache
$this->record = null;
return true;
}
public function expire($id, $ttl) {
// Destroy session record if expire is now.
if ($ttl == 0)
return $this->destroy($id);
if (!$this->expireRecord($id, $ttl))
return false;
try {
if (isset($this->secondary))
$this->secondary->expireRecord($id, $ttl);
} catch (\Trowable $t) {
// Ignore any BS!
}
return true;
}
public function destroy($id) {
if (!($this->destroyRecord($id)))
return false;
try {
if (isset($this->secondary))
$this->secondary->destroyRecord($id);
} catch (\Trowable $t) {
// Ignore any BS!
}
return true;
}
public function cleanup() {
$this->cleanupExpiredRecords();
try {
if (isset($this->secondary))
$this->secondary->cleanupExpiredRecords();
} catch (\Trowable $t) {
// Ignore any BS!
}
return true;
}
// Backend must implement lookup method to return a record that
// implements SessionRecordInterface.
abstract function lookupRecord($id, $autocreate);
// save record
abstract function saveRecord($record, $secondary = false);
// writeRecord is useful when replicating records without the need
// to transcode it when backends are different
abstract function writeRecord($id, $data);
// expireRecord
abstract function expireRecord($id, $ttl);
// Backend should implement destroyRecord that takes $id to avoid
// the need to do record lookup
abstract function destroyRecord($id);
// Clear expired records - backend knows best how
abstract function cleanupExpiredRecords();
}
abstract class AbstractMemcacheSessionStorageBackend
extends AbstractSessionStorageBackend {
private $memcache;
private $servers = [];
public function __construct($options) {
parent::__construct($options);
// Make sure we have memcache module installed
if (!extension_loaded('memcache'))
throw new Exception('Memcache extension is missing');
// Require servers to be defined
if (!isset($options['servers'])
|| !is_array($options['servers'])
|| !count($options['servers']))
throw new Exception('Memcache severs required');
// Init Memchache module
$this->memcache = new \Memcache();
// Add servers
foreach ($options['servers'] as $server) {
list($host, $port) = explode(':', $server);
// Use port '0' for unix sockets
if (strpos($host, '/') !== false)
$port = 0;
elseif (!$port)
$port = ini_get('memcache.default_port') ?: 11211;
$this->addServer(trim($host), (int) trim($port));
}
if (!$this->getNumServers())
throw new Exception('At least one memcache severs required');
// Init Memchache module
$this->memcache = new \Memcache();
}
protected function addServer($host, $port) {
// TODO: Support add options
// FIXME: Crash or warn if invalid $host or $port
// Cache Servers locally
$this->servers[] = [$host, $port];
// Add Server
$this->memcache->addServer($host, $port);
}
protected function getNumServers() {
return count($this->getServers());
}
protected function getServers() {
return $this->servers;
}
protected function getKey($id) {
return sha1($id.SECRET_SALT);
}
protected function get($id) {
// get key
$key = $this->getKey($id);
// Attempt distributed read
$data = $this->memcache->get($key);
// Read from other servers on failure
if ($data === false
&& $this->getNumServers()) {
foreach ($this->getServers() as $server) {
list($host, $port) = $server;
$this->memcache->pconnect($host, $port);
if ($data = $this->memcache->get($key))
break;
}
}
return $data;
}
protected function set($id, $data) {
// Since memchache takes care of carbage collection internally
// we want to make sure we set data to expire based on the session's
// maxidletime (if available) otherwise it defailts to SESSION_TTL.
// Memchache has a maximum ttl of 30 days
$ttl = min($this->getMaxlife(), 2592000);
$key = $this->getKey($id);
foreach ($this->getServers() as $server) {
list($host, $port) = $server;
$this->memcache->pconnect($host, $port);
if (!$this->memcache->replace($key, $data, 0, $ttl))
$this->memcache->set($key, $data, 0, $ttl);
}
// FIXME: Return false if we fail to write to at least one server
return true;
}
public function expireRecord($id, $ttl) {
return true;
}
public function destroyRecord($id) {
$key = $this->getKey($id);
foreach ($this->getServers() as $server) {
list($host, $port) = $server;
$this->memcache->pconnect($host, $port);
$this->memcache->replace($key, '', 0, 1);
$this->memcache->delete($key, 0);
}
return true;
}
public function cleanupExpiredRecords() {
// Memcache does this automatically
return true;
}
abstract function writeRecord($id, $data);
abstract function saveRecord($record, $secondary = false);
}
/*
* NoopSessionHandler
*
* Use this session handler when you don't care about session data.
*
*/
class NoopSessionStorageBackend extends AbstractSessionHandler {
public function read($id) {
return "";
}
public function update($id, $data) {
return true;
}
public function expire($id, $ttl) {
return true;
}
public function destroy($id) {
return true;
}
public function gc($maxlife) {
return true;
}
public function cleanup() {
return true;
}
}
// Delegate everything to PHP Default SessionHandler
class SystemSessionHandler extends \SessionHandler {
public function __construct() {
}
}
}

View File

@@ -18,8 +18,8 @@ class SetupWizard {
//Mimimum requirements
static protected $prereq = array(
'php' => '8.0',
'mysql' => '5.0');
'php' => '8.2',
'mysql' => '5.5');
//Version info - same as the latest version.

View File

@@ -713,7 +713,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
'child_thread__referrals__team__team_id__in' => $teams)));
$assigned->add($childRefTeam);
}
$visibility = Q::any(new Q(array('status__state'=>'open', $assigned)));
$visibility = Q::any(new Q(array('status__state__in'=>['open', 'closed'], $assigned)));
// -- If access is limited to assigned only, return assigned
if ($this->isAccessLimited())
return $visibility;
@@ -738,7 +738,7 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
}
function applyVisibility($query, $exclude_archived=false) {
return $query->filter($this->getTicketsVisibility($exclude_archived));
return $query->filter($this->getTicketsVisibility($exclude_archived))->distinct('ticket_id');
}
function applyDeptVisibility($qs) {
@@ -1503,6 +1503,7 @@ extends AbstractForm {
'required' => true,
'configuration' => array(
'classes' => 'span12',
'length' => '128',
),
'visibility' => new VisibilityConstraint(
new Q(array('welcome_email' => false)),
@@ -1522,6 +1523,7 @@ extends AbstractForm {
'required' => true,
'configuration' => array(
'classes' => 'span12',
'length' => '128',
),
'visibility' => new VisibilityConstraint(
new Q(array('welcome_email' => false)),
@@ -1565,6 +1567,7 @@ extends AbstractForm {
'required' => true,
'configuration' => array(
'autofocus' => true,
'length' => '128',
),
'validator' => 'noop',
)),
@@ -1572,6 +1575,9 @@ extends AbstractForm {
'label' => __('Enter a new password'),
'placeholder' => __('New Password'),
'required' => true,
'configuration' => array(
'length' => '128',
),
'validator' => '',
'validators' => function($self, $v) {
try {
@@ -1584,6 +1590,9 @@ extends AbstractForm {
'passwd2' => new PasswordField(array(
'placeholder' => __('Confirm Password'),
'required' => true,
'configuration' => array(
'length' => '128',
),
'validator' => '',
'validators' => function($self, $v) {
try {

View File

@@ -350,23 +350,21 @@ class Task extends TaskModel implements RestrictedAccess, Threadable {
}
function getLastRespondent() {
if (!isset($this->lastrespondent)) {
$this->lastrespondent = Staff::objects()
->filter(array(
'staff_id' => static::objects()
'staff_id' => $this->getThread()->entries
->filter(array(
'thread__entries__type' => 'R',
'thread__entries__staff_id__gt' => 0
'type' => 'R',
'staff_id__gt' => 0
))
->values_flat('thread__entries__staff_id')
->order_by('-thread__entries__id')
->limit('1,1')
->values_flat('staff_id')
->order_by('-id')
->limit('1')
))
->first()
?: false;
}
return $this->lastrespondent;
}

View File

@@ -897,8 +897,8 @@ implements TemplateVariable {
// a response to an existing thread entry
if ($ost)
$ost->log(LOG_ERR, _S('Email loop detected'), sprintf(
_S('It appears as though %s is being used as a forwarded or fetched email account and is also being used as a user / system account. Please correct the loop or seek technical assistance.'),
$mailinfo['email']),
_S('It appears as though %s is being used as a forwarded or fetched email account and is also being used as a user / system account. Please correct the loop or seek technical assistance.\n\n%s'),
$mailinfo['email'], $mailinfo['header']),
// This is quite intentional -- don't continue the loop
false,
@@ -1665,13 +1665,14 @@ implements TemplateVariable {
$entry = new static(array(
'created' => SqlFunction::NOW(),
'updated' => SqlFunction::NOW(),
'type' => $vars['type'],
'thread_id' => $vars['threadId'],
'title' => Format::strip_emoticons(Format::sanitize($vars['title'], true)),
'format' => $vars['body']->getType(),
'staff_id' => $vars['staffId'],
'user_id' => $vars['userId'],
'poster' => $poster,
'poster' => Format::sanitize($poster),
'source' => $vars['source'],
'flags' => $vars['flags'] ?: 0,
));
@@ -1756,6 +1757,10 @@ implements TemplateVariable {
$files[$i]['inline'] = true;
}
foreach ($entry->normalizeFileInfo($files) as $F) {
if (!empty($F['inline'])
&& (!isset($F['file']) || !$F['file']->isInlineSafeImage()))
$F['inline'] = false;
// Deduplicate on the `key` attribute. The key is
// necessary for the CID rewrite below
$attached_files[$F['key']] = $F;
@@ -1768,6 +1773,9 @@ implements TemplateVariable {
// discarded, only the unique hash-code (key) will be available to
// retrieve the image later
foreach ($attached_files as $key => $a) {
if (empty($a['inline']))
continue;
if (isset($a['cid']) && $a['cid']) {
$body = preg_replace('/src=("|\'|\b)(?:cid:)?'
. preg_quote($a['cid'], '/').'\1/i',
@@ -2115,6 +2123,7 @@ class ThreadEvent extends VerySimpleModel {
case 'timestamp':
$timeFormat = null;
if ($mode != self::MODE_CLIENT && $thisstaff
&& (!isset($_REQUEST['a']) || $_REQUEST['a']!='print')
&& !strcasecmp($thisstaff->datetime_format,
'relative')) {
$timeFormat = function ($timestamp) {
@@ -2200,8 +2209,8 @@ class ThreadEvent extends VerySimpleModel {
$inst = self::create(array(
'thread_type' => ObjectModel::OBJECT_TYPE_TICKET,
'staff_id' => $staff,
'team_id' => $ticket->getTeamId(),
'staff_id' => $staff ?: 0,
'team_id' => $ticket->getTeamId() ?: 0,
'dept_id' => $ticket->getDeptId(),
'topic_id' => $ticket->getTopicId(),
), $user);
@@ -2211,8 +2220,8 @@ class ThreadEvent extends VerySimpleModel {
static function forTask($task, $state, $user=false) {
$inst = self::create(array(
'thread_type' => ObjectModel::OBJECT_TYPE_TASK,
'staff_id' => $task->getStaffId(),
'team_id' => $task->getTeamId(),
'staff_id' => $task->getStaffId() ?: 0,
'team_id' => $task->getTeamId() ?: 0,
'dept_id' => $task->getDeptId(),
), $user);
return $inst;
@@ -2285,7 +2294,7 @@ class Event extends VerySimpleModel {
static function getStates($dropdown=false) {
$names = array();
if ($dropdown)
$names = array(__('All'));
$names = array('All');
$events = self::objects()->values_flat('name');
foreach ($events as $val)

View File

@@ -715,7 +715,7 @@ implements RestrictedAccess, Threadable, Searchable {
if (!$lock->delete())
return false;
$this->lock = null;
$this->lock = 0;
return $this->save();
}
@@ -2725,7 +2725,7 @@ implements RestrictedAccess, Threadable, Searchable {
&& ($msg=$tpl->getTransferAlertMsgTemplate())
) {
$msg = $this->replaceVars($msg->asArray(),
array('comments' => $note, 'staff' => $thisstaff));
array('comments' => $note ?: '', 'staff' => $thisstaff));
// Recipients
$recipients = array();
// Assigned staff or team... if any
@@ -4237,7 +4237,8 @@ implements RestrictedAccess, Threadable, Searchable {
$errors += $form->errors();
if ($vars['topicId']) {
if (($topic=Topic::lookup($vars['topicId']))
if (is_numeric($vars['topicId'])
&& ($topic=Topic::lookup((int) $vars['topicId']))
&& $topic->isActive()) {
foreach ($topic_forms as $topic_form) {
$TF = $topic_form->getForm($vars);
@@ -4369,7 +4370,7 @@ implements RestrictedAccess, Threadable, Searchable {
$ticket->email_id = $vars['emailId'];
//Make sure the origin is staff - avoid firebug hack!
if ($vars['duedate'] && !strcasecmp($origin,'staff'))
if ($vars['duedate'] && in_array(strtolower($origin), ['staff', 'api']))
$ticket->duedate = date('Y-m-d G:i',
Misc::dbtime($vars['duedate']));
@@ -4650,7 +4651,6 @@ implements RestrictedAccess, Threadable, Searchable {
// post response - if any
$response = null;
if ($vars['response'] && $role->hasPerm(Ticket::PERM_REPLY)) {
$vars['response'] = $ticket->replaceVars($vars['response']);
// $vars['cannedatachments'] contains the attachments placed on
// the response form.
$response = $ticket->postReply($vars, $errors, ($alert &&

View File

@@ -776,7 +776,9 @@ class TextDomain {
return;
// Define locale for C-libraries
putenv('LC_ALL=' . $info['code']);
// Check if putenv() is available - some hosts disable it
if (function_exists('putenv'))
putenv('LC_ALL=' . $info['code']);
self::setLocale(LC_ALL, $info['code']);
}

View File

@@ -203,7 +203,7 @@ class StreamUpgrader extends SetupWizard {
if($email) {
$email->sendAlert($thisstaff->getEmail(), $subject, $error);
} else {//no luck - try the system mail.
Mailer::sendmail($thisstaff->getEmail(), $subject, $error,
osTicket\Mail\Mailer::sendmail($thisstaff->getEmail(), $subject, $error,
'"'._S('osTicket Alerts')."\" <{$thisstaff->getEmail()}>");
}

View File

@@ -220,7 +220,11 @@ implements TemplateVariable, Searchable {
static function fromVars($vars, $create=true, $update=false) {
// Try and lookup by email address
$user = static::lookupByEmail($vars['email']);
if (!$user && $create) {
if (!$user
// can create user?
&& $create
// Make sure at least email is valid
&& Validator::is_email($vars['email'])) {
$name = $vars['name'];
if (is_array($name))
$name = implode(', ', $name);
@@ -228,7 +232,7 @@ implements TemplateVariable, Searchable {
list($name) = explode('@', $vars['email'], 2);
$user = new User(array(
'name' => Format::htmldecode(Format::sanitize($name, false)),
'name' => trim(Format::htmldecode(Format::sanitize($name, false))),
'created' => new SqlFunction('NOW'),
'updated' => new SqlFunction('NOW'),
//XXX: Do plain create once the cause
@@ -254,8 +258,7 @@ implements TemplateVariable, Searchable {
$type = array('type' => 'created');
Signal::send('object.created', $user, $type);
Signal::send('user.created', $user);
}
elseif ($update) {
} elseif ($update && $user) {
$errors = array();
$user->updateInfo($vars, $errors, true);
}
@@ -586,7 +589,7 @@ implements TemplateVariable, Searchable {
$type = array('type' => 'edited', 'key' => 'Name');
Signal::send('object.edited', $this, $type);
}
$this->name = $name;
$this->name = trim($name);
}
// Email address field
@@ -1326,7 +1329,7 @@ class UserAccount extends VerySimpleModel {
}
$this->set('timezone', $vars['timezone']);
$this->set('username', $vars['username']);
$this->set('username', Format::sanitize($vars['username']));
if ($vars['passwd1']) {
$this->setPassword($vars['passwd1']);
@@ -1404,7 +1407,7 @@ class UserAccount extends VerySimpleModel {
));
if ($vars['username'] && strcasecmp($vars['username'], $user->getEmail()))
$account->set('username', $vars['username']);
$account->set('username', Format::sanitize($vars['username']));
if ($vars['passwd1'] && !$vars['sendemail']) {
$account->set('passwd', Passwd::hash($vars['passwd1']));

View File

@@ -2,10 +2,13 @@
/*********************************************************************
class.usersession.php
User (client and staff) sessions handle.
User (client and staff) sessions manager
User-Space session management, not to confused with Session Storage
Backends.
Peter Rotich <peter@osticket.com>
Copyright (c) 2006-2013 osTicket
Copyright (c) 2022 osTicket
http://www.osticket.com
Released under the GNU General Public License WITHOUT ANY WARRANTY.
@@ -52,9 +55,12 @@ class UserSession {
}
function sessionToken(){
// Please note that user-space token is not meant to be secure at all
// we're simply encoding stuff we want to track as we refresh the
// session.
$time = time();
$hash = md5($time.SESSION_SECRET.$this->userID);
$token = "$hash:$time:".MD5($this->ip);
$token = "$hash:$time:".MD5($this->getIP());
return $token;
}
@@ -66,35 +72,31 @@ class UserSession {
return $expire;
}
function isvalidSession($htoken,$maxidletime=0,$checkip=false){
global $cfgi;
function isvalidSession($htoken, $maxidletime=0, $checkip=false){
global $cfg;
// Compare session ids
if (strcmp($this->getSessionId(), session_id()))
return false;
// Is the session invalidated?
if (isset($_SESSION['KAPUT']) && $_SESSION['KAPUT'] < time())
return (session_destroy() && false);
$token = rawurldecode($htoken);
// Check if we got what we expected....
if ($token && !strstr($token,":"))
return false;
// Get the goodies
list($hash, $expire, $ip) = explode(":",$token);
list($hash, $expire, $ip) = explode(':', $token);
// Make sure the session hash is valid
if ((md5($expire . SESSION_SECRET . $this->userID) != $hash))
if (!hash_equals(md5($expire . SESSION_SECRET . $this->userID), $hash))
return false;
// is it expired??
if ($maxidletime && ((time()-$expire)>$maxidletime))
if ($maxidletime && ((time()-$expire) > $maxidletime))
return false;
#Make sure IP is still same ( proxy access??????)
if ($checkip && strcmp($ip, MD5($this->ip)))
// Make sure IP is still same - if requested
if ($checkip && strcmp($ip, MD5($this->getIP())))
return false;
$this->validated = true;
@@ -102,54 +104,13 @@ class UserSession {
return true;
}
function regenerateSession($destroy=false) {
// Delayed kaput time for current session
$_SESSION['KAPUT'] = time() + 60;
// Save the session id as old
$old = session_id();
// Regenerate the session without destroying data
session_regenerate_id(false);
// Get new session id and close
$new = session_id();
session_write_close();
// Start new session
session_id($new);
session_start();
$this->session_id = $new;
// Make sure new session is not set to KAPUT and TIME_BOMB
unset($_SESSION['KAPUT'], $_SESSION['TIME_BOMB']);
// Destroy ?
if ($destroy) {
// Destrory old session
$this->destroySession($old);
// Restore new session
session_id($new);
session_start();
}
return true;
}
function destroySession($id) {
// Close current session
session_write_close();
// Start target session
session_id($id);
session_start();
// Destroy session
session_destroy();
session_write_close();
return true;
}
function isValid() {
return ($this->validated);
}
}
trait UserSessionTrait {
// Session Object
// User Session Object
var $session;
// Session Token
var $token;
@@ -160,23 +121,60 @@ trait UserSessionTrait {
// User class
var $class = '';
function refreshSession($refreshRate=60): void {
// If TIME_BOMB isset and less than the current time we need to regenerate
// session to help mitigate session fixation
if (isset($_SESSION['TIME_BOMB']) && ($_SESSION['TIME_BOMB'] < time()))
$this->regenerateSession();
public function getMaxIdleTime() {
return $this->maxidletime ;
}
function refreshSession($refreshRate=60) {
// Check Time To Die (TTD) if any - OLD people.. I mean sessions,
// must die! Don't fight it bro!
if (isset($_SESSION['TTD']) && $_SESSION['TTD'] < time()) {
error_log(sprintf('Session %s with TTD %s was used',
session_id(), $_SESSION['TTD']));
return (session_destroy() && false);
}
// If TIME_BOMB is set and less than the current time we need to regenerate
// session id to help mitigate session fixation attacks.
// Only regenerate on GET to avoid invalidating data in-flight on a
// POST request
if ($_SERVER['REQUEST_METHOD'] === 'GET'
&& isset($_SESSION['TIME_BOMB'])
&& ($_SESSION['TIME_BOMB'] < time())
&& ($id=$this->regenerateSession())) {
// unset timer and set next one based on maxlife for the user or
// 24 hrs later
// TODO: Make regenerate frequency configurable in 2032 /j
// PS: Living and dying and the stories that are true Secrets to
// a good life is knowing when you're through ~ time bomb
$ttl = ($this->getMaxIdleTime() ?: 86400);
$_SESSION['TIME_BOMB'] = time() + $ttl;
// Set new id locally
$this->session_id = $id;
// Force cookie renewal NOW!
$refreshRate = -1;
}
// Deadband session token updates to once / 30-seconds
$updated = $this->session->getLastUpdate($this->token);
if ($updated + $refreshRate < time()) {
// Renew the session token
$this->token = $this->getSessionToken();
osTicketSession::renewCookie($updated, $this->maxidletime);
// Update the expire time for the session cookie
osTicketSession::renewCookie(time(), $this->getMaxIdleTime());
}
}
function regenerateSession($destroy=false) {
$this->session->regenerateSession($destroy);
// Set cookie for the new session id.
$this->refreshSession(-1);
function regenerateSession(int $ttl = 120) {
// Set TTD (Time To Die) on current session
// If ttl is 0 then session is destroyed immediatetly
$_SESSION['TTD'] = time() + $ttl; // now + ttl
if (($id=osTicketSession::regenerate($ttl)))
$this->session->session_id = $id;
// unset TTD on the new session - new life my boy!
unset($_SESSION['TTD']);
return $id;
}
function getSession() {
@@ -202,7 +200,7 @@ trait UserSessionTrait {
function isValidSession() {
return ($this->getId()
&& $this->session->isvalidSession($this->token,
$this->maxidletime, $this->checkip));
$this->getMaxIdleTime(), $this->checkip));
}
abstract function isValid();
@@ -216,7 +214,7 @@ class ClientSession extends EndUser {
parent::__construct($user);
$this->class ='client';
// XXX: Change the key to user-id
$this->session = new UserSession($user->getId());
$this->session = new UserSession($user->getUserId());
$this->setSessionToken();
$this->maxidletime = $cfg->getClientTimeout();
}

View File

@@ -98,12 +98,12 @@ implements IteratorAggregate, Countable {
}
// IteratorAggregate
function getIterator() {
function getIterator(): Traversable {
return new ArrayIterator($this->storage);
}
// Countable
function count($mode=COUNT_NORMAL) {
function count($mode=COUNT_NORMAL): int {
return count($this->storage, $mode);
}
@@ -195,7 +195,7 @@ implements ArrayAccess, Serializable {
}
// ArrayAccess
function offsetGet($offset) {
function offsetGet($offset): mixed {
if (!is_int($offset))
throw new InvalidArgumentException('List indices should be integers');
elseif ($offset < 0)
@@ -204,10 +204,11 @@ implements ArrayAccess, Serializable {
throw new OutOfBoundsException('List index out of range');
return $this->storage[$offset];
}
function offsetSet($offset, $value) {
if ($offset === null)
return $this->storage[] = $value;
elseif (!is_int($offset))
function offsetSet($offset, $value): void {
if ($offset === null) {
$this->storage[] = $value;
return;
} elseif (!is_int($offset))
throw new InvalidArgumentException('List indices should be integers');
elseif ($offset < 0)
$offset += count($this->storage);
@@ -217,14 +218,14 @@ implements ArrayAccess, Serializable {
$this->storage[$offset] = $value;
}
function offsetExists($offset) {
function offsetExists($offset): bool {
if (!is_int($offset))
throw new InvalidArgumentException('List indices should be integers');
elseif ($offset < 0)
$offset += count($this->storage);
return isset($this->storage[$offset]);
}
function offsetUnset($offset) {
function offsetUnset($offset): void {
if (!is_int($offset))
throw new InvalidArgumentException('List indices should be integers');
elseif ($offset < 0)

View File

@@ -185,7 +185,7 @@ class Validator {
// full-stop trailing char so that the default domain of the server
// is not added automatically
if ($verify and !dns_get_record($m->host.'.', DNS_MX))
return 0 < @count(dns_get_record($m->host.'.', DNS_A|DNS_AAAA));
return (count(dns_get_record($m->host.'.', DNS_A|DNS_AAAA) ?: []));
return true;
}
@@ -229,9 +229,9 @@ class Validator {
return $error == '';
}
static function is_userid($userid, &$error='') {
static function is_userid($userid, &$error='', $verify=false) {
if (!self::is_username($userid)
&& !self::is_email($userid))
&& !self::is_email($userid, false, $verify))
$error = __('Invalid User Id ');
return $error == '';
}
@@ -367,7 +367,8 @@ class Validator {
$aclbk = $cfg->getACLBackend();
switch($backend) {
case 'client':
if (in_array($aclbk, array(0,3)))
if (in_array($aclbk, array(0,3))
|| ($aclbk == 2 && StaffAuthenticationBackend::getUser()))
return true;
break;
case 'staff':

View File

@@ -71,6 +71,15 @@ class VariableReplacer {
$tag = mb_strtolower($tag);
$rv = null;
// Deny sensitive attributes outright
$blacklist = [
'passwd',
'password',
'authkey',
];
if (in_array($tag, $blacklist))
return "";
if (!is_object($obj)) {
if ($tag && is_array($obj) && array_key_exists($tag, $obj))
$rv = $obj[$tag];

View File

@@ -91,14 +91,14 @@ class AgentManager extends Module {
if (!($this->stream = fopen($stream, 'c')))
$this->fail("Unable to open output file [{$options['file']}]");
fputcsv($this->stream, array('First Name', 'Last Name', 'Email', 'UserName'));
fputcsv($this->stream, array('First Name', 'Last Name', 'Email', 'UserName'), ",", "\"", "");
foreach ($this->getAgents($options) as $agent)
fputcsv($this->stream, array(
$agent->getFirstName(),
$agent->getLastName(),
$agent->getEmail(),
$agent->getUserName(),
));
), ",", "\"", "");
break;
case 'list':

View File

@@ -87,7 +87,7 @@ class Deployment extends Unpacker {
$root, $recurse - 1, $exclude);
}
}
if (!$contents || !glob($destination.'{,.}*', GLOB_BRACE|GLOB_NOSORT)) {
if (!$contents || !empty(glob($destination.'{,.}*', GLOB_BRACE|GLOB_NOSORT))) {
if ($verbose)
$this->stdout->write("(delete-folder): $destination\n");
if (!$dryrun)
@@ -205,7 +205,13 @@ class Deployment extends Unpacker {
$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);
list($mode, $hash, , $path, $pathx, $pathy, $pathz) = preg_split('/\s+/', $line);
if (isset($pathx))
$path = "$path $pathx";
if (isset($pathy))
$path = "$path $pathy";
if (isset($pathz))
$path = "$path $pathz";
$src = $source.$local.$path;
if ($this->exclude($exclude, $src))
continue;

View File

@@ -53,10 +53,10 @@ class i18n_Compiler extends Module {
profile in your home folder or in your environment. See AWS
configuration docs for more information";
static $project = 'osticket-official';
static $crowdin_api_url = 'http://i18n.osticket.com/api/project/{project}/{command}';
static $project_id = '25755';
static $crowdin_api_url = 'http://i18n.osticket.com/api/v2/projects/{project_id}/{command}';
function _http_get($url) {
function _http_get($url, $headers=[], $fields=[]) {
$this->stdout->write(">>> Downloading $url\n");
#curl post
$ch = curl_init();
@@ -65,6 +65,15 @@ class i18n_Compiler extends Module {
curl_setopt($ch, CURLOPT_HEADER, FALSE);
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, FALSE);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE);
$headers[] = 'Cache-Control: no-cache';
// Set POST data
if (!empty($fields)) {
$headers[] = 'Content-Type: application/json';
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($fields));
}
// Set headers
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
$result=curl_exec($ch);
$code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
@@ -72,18 +81,18 @@ class i18n_Compiler extends Module {
return array($code, $result);
}
function _request($command, $args=array()) {
$url = str_replace(array('{command}', '{project}'),
array($command, self::$project),
function _request($command, $args=array(), $fields=[]) {
$url = str_replace(array('{command}', '{project_id}'),
array($command, self::$project_id),
self::$crowdin_api_url);
$args += array('key' => $this->key);
if ($branch = $this->getOption('branch', false))
$args += array('branch' => $branch);
$url .= '?' . Http::build_query($args);
// Set Personal Access Token for every request
$headers = ['Authorization: Bearer '.$this->key];
// Build URL with arguments
if (!empty($args))
$url .= '?' . Http::build_query($args);
return $this->_http_get($url);
return $this->_http_get($url, $headers, $fields);
}
function run($args, $options) {
@@ -128,20 +137,23 @@ class i18n_Compiler extends Module {
function _list() {
error_reporting(E_ALL);
list($code, $body) = $this->_request('status');
$d = new DOMDocument();
$d->loadXML($body);
// Get languages progress
list($code, $body) = $this->_request('languages/progress', ['limit' => 300]);
// Fail if not successful
if ($code !== 200)
$this->fail("Request failed ({$code}): ".JsonDataParser::parse($body)['error']['message']."\n");
$xp = new DOMXpath($d);
foreach ($xp->query('//language') as $c) {
// Parse response to get data
$langs = JsonDataParser::parse($body)['data'];
foreach ($langs as $lang) {
$name = $code = '';
foreach ($c->childNodes as $n) {
switch (strtolower($n->nodeName)) {
foreach ($lang['data']['language'] as $n=>$v) {
switch (strtolower($n)) {
case 'name':
$name = $n->textContent;
$name = $v;
break;
case 'code':
$code = $n->textContent;
case 'id':
$code = $v;
break;
}
}
@@ -152,8 +164,34 @@ class i18n_Compiler extends Module {
}
function _build($lang, $options) {
list($code, $zip) = $this->_request("download/$lang.zip");
// Set target language
$fields = ['targetLanguageIds' => [$lang]];
// Set branch ID from branch name
if ($branch = $this->getOption('branch', false))
$fields['branchId'] = $this->getBranchId($branch);
// List available builds for language
list($code, $result) = $this->_request("translations/builds", [], $fields);
// Parse response and return build ID
// Fail if no build ID as it's required
if ($code !== 201 || !($build_id = JsonDataParser::parse($result)['data']['id']))
$this->fail("No builds available for target language");
$progress = 0;
while ($progress<100) {
sleep(2);
list ($c, $r) = $this->_request("translations/builds/{$build_id}");
if ($c !== 200)
$this->fail("No builds available for target language");
$progress = (int) JsonDataParser::parse($r)['data']['progress'];
}
// Request the download information
list($code, $body) = $this->_request("translations/builds/{$build_id}/download");
// Parse response and return download URL - fail if none
if (!($url = JsonDataParser::parse($body)['data']['url']))
$this->fail('Selected build not available for target language');
// Download using the URL in the response
list($code, $zip) = $this->_http_get($url);
// Fail if not successful
if ($code !== 200)
$this->fail('Language is not available'."\n");
@@ -181,10 +219,6 @@ class i18n_Compiler extends Module {
$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;
@@ -291,21 +325,21 @@ class i18n_Compiler extends Module {
}
// Include a manifest
include_once INCLUDE_DIR . 'class.mailfetch.php';
include_once INCLUDE_DIR . 'class.mailparse.php';
$po_header = Mail_Parse::splitHeaders($mo['']);
$info = array(
'Build-Date' => date(DATE_RFC822),
'Phrases-Version' => $po_header['X-Osticket-Major-Version'],
'Phrases-Version' => $po_header['x-osticket-major-version'],
'Build-Version' => trim(`git describe`),
'Build-Major-Version' => MAJOR_VERSION,
'Language' => $po_header['Language'],
'Language' => $po_header['language'],
#'Phrases' =>
#'Translated' =>
#'Approved' =>
'Id' => 'lang:' . $lang,
'Last-Revision' => $po_header['PO-Revision-Date'],
'Version' => (int)(strtotime($po_header['PO-Revision-Date']) / 10000),
'Last-Revision' => $po_header['po-revision-date'],
'Version' => (int)(strtotime($po_header['po-revision-date']) / 10000),
);
$phar->addFromString(
'MANIFEST.php',
@@ -353,34 +387,42 @@ class i18n_Compiler extends Module {
$this->fail('Unable to include AWS phar file. Download to INCLUDE_DIR');
require_once INCLUDE_DIR . 'aws.phar';
$aws = Aws\Common\Aws::factory(array());
$client = $aws->get('Route53');
// Services only available via global endpoints (eg. Route 53)
// must be instantiated with 'us-east-1'
$aws = new Aws\Sdk([
'region' => 'us-east-1',
'version' => '2013-04-01',
'credentials' => [
'key' => getenv('AWS_ACCESS_KEY_ID'),
'secret' => getenv('AWS_SECRET_ACCESS_KEY')
]
]);
$client = $aws->createRoute53();
try {
$resp = $client->changeResourceRecordSets(array(
'HostedZoneId' => $options['dns'],
'ChangeBatch' => array(
'Changes' => array(
array(
'Action' => 'CREATE',
'ResourceRecordSet' => array(
'Name' => "{$signature['hash']}.updates.osticket.com.",
'Type' => 'TXT',
'TTL' => 172800,
'ResourceRecords' => array(
array(
'Value' => $seal,
$resp = $client->changeResourceRecordSets(array(
'HostedZoneId' => $options['dns'],
'ChangeBatch' => array(
'Changes' => array(
array(
'Action' => 'CREATE',
'ResourceRecordSet' => array(
'Name' => "{$signature['hash']}.updates.osticket.com.",
'Type' => 'TXT',
'TTL' => 172800,
'ResourceRecords' => array(
array(
'Value' => $seal,
),
),
),
),
),
),
),
));
$this->stdout->write(sprintf('%s: %s', $resp['ChangeInfo']['Comment'],
$resp['ChangeInfo']['Status']));
}
catch (Exception $ex) {
));
$this->stdout->write(sprintf('%s: %s', $resp['ChangeInfo']['Comment'],
$resp['ChangeInfo']['Status']));
} catch (Exception $ex) {
$this->stdout->write("Seal: $seal\n");
$this->fail('!! AWS Update Failed: '.$ex->getMessage());
}
@@ -390,10 +432,11 @@ class i18n_Compiler extends Module {
}
}
function __read_next_string($tokens) {
function __read_next_string(&$tokens) {
$string = array();
foreach ($tokens as $x=>$T) {
while ($T = current($tokens)) {
next($tokens);
switch ($T[0]) {
case T_CONSTANT_ENCAPSED_STRING:
// Strip leading and trailing ' and " chars
@@ -431,7 +474,7 @@ class i18n_Compiler extends Module {
}
}
}
function __read_args($tokens, $proto=false) {
function __read_args(&$tokens, $proto=false) {
$args = array('forms'=>array());
$arg = null;
$proto = $proto ?: array('forms'=>1);
@@ -467,8 +510,9 @@ class i18n_Compiler extends Module {
}
}
function __get_func_args($tokens, $args) {
foreach ($tokens as $x=>$T) {
function __get_func_args(&$tokens, $args) {
while ($T = current($tokens)) {
next($tokens);
switch ($T[0]) {
case T_WHITESPACE:
continue 2;
@@ -480,10 +524,11 @@ class i18n_Compiler extends Module {
}
}
}
function __find_strings($tokens, $funcs, $parens=0) {
function __find_strings(&$tokens, $funcs, $parens=0) {
$T_funcs = array();
$funcdef = false;
foreach ($tokens as $x=>$T) {
while ($T = current($tokens)) {
next($tokens);
switch ($T[0]) {
case T_STRING:
case T_VARIABLE:
@@ -640,7 +685,7 @@ class i18n_Compiler extends Module {
foreach ($files as $f) {
$F = str_replace($root.'/', $domain, $f);
$this->stderr->write("$F\n");
$tokens = new ArrayObject(token_get_all(fread(fopen($f, 'r'), filesize($f))));
$tokens = token_get_all(fread(fopen($f, 'r'), filesize($f)));
foreach ($this->__find_strings($tokens, $funcs, 1) as $call) {
self::__addString($strings, $call, $F);
}
@@ -713,7 +758,8 @@ class i18n_Compiler extends Module {
preg_match_all('/(?:function\s+)?__\(\s*[^\'"]*(([\'"])(?:(?<!\\\\)\2|.)+\2)\s*[^)]*\)/',
$script, $calls, PREG_OFFSET_CAPTURE | PREG_SET_ORDER);
foreach ($calls as $c) {
if (!($call = $this->__find_strings(token_get_all('<?php '.$c[0][0]), $funcs, 0)))
$tokens = token_get_all('<?php '.$c[0][0]);
if (!($call = $this->__find_strings($tokens, $funcs, 0)))
continue;
$call = $call[0];
@@ -725,6 +771,13 @@ class i18n_Compiler extends Module {
}
return $strings;
}
function getBranchId($branch) {
list($code, $result) = $this->_request("branches", ['name' => (string) $branch]);
if ($code !== 200)
$this->fail('Branch not available');
return (int) JsonDataParser::parse($result)['data'][0]['data']['id'];
}
}
Module::register('i18n', 'i18n_Compiler');

View File

@@ -59,11 +59,11 @@ class ListManager extends Module {
if (!($this->stream = fopen($stream, 'c')))
$this->fail("Unable to open output file [{$options['file']}]");
fputcsv($this->stream, array('Value', 'Abbrev'));
fputcsv($this->stream, array('Value', 'Abbrev'), ",", "\"", "");
foreach ($list->getItems() as $item)
fputcsv($this->stream, array(
(string) $item->getValue(),
$item->getAbbrev()));
$item->getAbbrev()), ",", "\"", "");
break;
case 'show':
$lists = DynamicList::objects()->order_by('-type', 'name');

View File

@@ -33,7 +33,7 @@ class OrganizationManager extends Module {
$this->fail("Unable to open input file [{$options['file']}]");
//Read the header (if any)
if (($data = fgetcsv($this->stream, 1000, ","))) {
if (($data = fgetcsv($this->stream, 1000, ",", "\"", ""))) {
if (strcasecmp($data[0], 'name'))
fseek($this->stream, 0); // We don't have an header!
else;
@@ -41,7 +41,7 @@ class OrganizationManager extends Module {
// for now we're assuming one column of Name
}
while (($data = fgetcsv($this->stream, 1000, ",")) !== FALSE) {
while (($data = fgetcsv($this->stream, 1000, ",", "\"", "")) !== FALSE) {
if (!$data[0])
$this->stderr->write('Invalid data format: Name
required');
@@ -55,10 +55,10 @@ class OrganizationManager extends Module {
if (!($this->stream = fopen($stream, 'c')))
$this->fail("Unable to open output file [{$options['file']}]");
fputcsv($this->stream, array('Name'));
fputcsv($this->stream, array('Name'), ",", "\"", "");
foreach (Organization::objects() as $org)
fputcsv($this->stream,
array((string) $org->getName()));
array((string) $org->getName()), ",", "\"", "");
break;
default:
$this->stderr->write('Unknown action!');

View File

@@ -76,10 +76,10 @@ class UserManager extends Module {
if (!($this->stream = fopen($stream, 'c')))
$this->fail("Unable to open output file [{$options['file']}]");
fputcsv($this->stream, array('Name', 'Email'));
fputcsv($this->stream, array('Name', 'Email'), ",", "\"", "");
foreach (User::objects() as $user)
fputcsv($this->stream,
array((string) $user->getName(), $user->getEmail()));
array((string) $user->getName(), $user->getEmail()), ",", "\"", "");
break;
case 'activate':
@@ -168,7 +168,8 @@ class UserManager extends Module {
default:
$this->stderr->write('Unknown action!');
}
@fclose($this->stream);
if (is_resource($this->stream))
@fclose($this->stream);
}
function getQuerySet($options, $requireOne=false) {

View File

@@ -6,7 +6,7 @@ $signin_url = ROOT_PATH . "login.php"
$signout_url = ROOT_PATH . "logout.php?auth=".$ost->getLinkToken();
header("Content-Type: text/html; charset=UTF-8");
header("Content-Security-Policy: frame-ancestors ".$cfg->getAllowIframes().";");
header("Content-Security-Policy: frame-ancestors ".$cfg->getAllowIframes()."; script-src 'self' 'unsafe-inline'; object-src 'none'");
if (($lang = Internationalization::getCurrentLanguage())) {
$langs = array_unique(array($lang, $cfg->getPrimaryLanguage()));
@@ -38,9 +38,9 @@ if (osTicket::is_ie())
<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/osticket.css" media="screen">
<link rel="stylesheet" href="<?php echo ASSETS_PATH; ?>css/theme.css" media="screen">
<link rel="stylesheet" href="<?php echo ASSETS_PATH; ?>css/print.css" media="print">
<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>scp/css/typeahead.css"
<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/typeahead.css"
media="screen" />
<link type="text/css" href="<?php echo ROOT_PATH; ?>css/ui-lightness/jquery-ui-1.13.1.custom.min.css"
<link type="text/css" href="<?php echo ROOT_PATH; ?>css/ui-lightness/jquery-ui-1.13.2.custom.min.css"
rel="stylesheet" media="screen" />
<link rel="stylesheet" href="<?php echo ROOT_PATH ?>css/jquery-ui-timepicker-addon.css" media="all">
<link rel="stylesheet" href="<?php echo ROOT_PATH; ?>css/thread.css" media="screen">
@@ -52,12 +52,12 @@ if (osTicket::is_ie())
<!-- Favicons -->
<link rel="icon" type="image/png" href="<?php echo ROOT_PATH ?>images/oscar-favicon-32x32.png" sizes="32x32" />
<link rel="icon" type="image/png" href="<?php echo ROOT_PATH ?>images/oscar-favicon-16x16.png" sizes="16x16" />
<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-3.5.1.min.js"></script>
<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.13.1.custom.min.js"></script>
<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-3.7.0.min.js"></script>
<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-1.13.2.custom.min.js"></script>
<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/jquery-ui-timepicker-addon.js"></script>
<script src="<?php echo ROOT_PATH; ?>js/osticket.js"></script>
<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/filedrop.field.js"></script>
<script src="<?php echo ROOT_PATH; ?>scp/js/bootstrap-typeahead.js"></script>
<script src="<?php echo ROOT_PATH; ?>js/bootstrap-typeahead.js"></script>
<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor.min.js"></script>
<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-plugins.js"></script>
<script type="text/javascript" src="<?php echo ROOT_PATH; ?>js/redactor-osticket.js"></script>

Some files were not shown because too many files have changed in this diff Show More