Compare commits

...

700 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
JediKev
1d8b79023e Add Release Notes for v1.17 2022-09-30 17:09:42 +00:00
JediKev
1eadd02354 Merge pull request #6290 from protich/issue/plugins-backwards-compatibility
Reviewed-By: aydreeihn <adriane@enhancesoft.com>, JediKev <kevin@enhancesoft.com>
2022-09-30 12:04:08 -05:00
Peter Rotich
3163972b1e plugin: Add getNamespace to Base 2022-09-30 15:30:40 +00:00
Peter Rotich
5787f9ae0d plugin: Add New Instance
This commit fixes a bug where blacklisted plugin cannot add a new instance post install.
2022-09-30 14:57:15 +00:00
Peter Rotich
7b26f6654a plugin: Make Plugin Base Class Nice
This commit addresses backward compatibility issue related to the data sent to
pre_save hook. Older plugins expected clean data whereas the new interface
provided to_db data which might be json formatted for example depending on the
config field database format. Now we will send the clean data to pre_save then
transform the results for database storage via the form / fields.
2022-09-30 14:48:28 +00:00
Peter Rotich
41b19db3f0 Merge pull request #6288 from protich/1.17.x
Reviewed-By: osTicket Devs <devs@osticket.com>
2022-09-29 12:46:21 -05:00
Peter Rotich
1b7f41aab3 Merge pull request #6287 from protich/1.17.x
Reviewed-By: osTicket Devs <devs@osticket.com>
2022-09-29 12:39:45 -05:00
Peter Rotich
65430abe20 Merge pull request #6285 from protich/issue/file-key-lookup
Reviewed-By: osTicket Devs <devs@osticket.com>
2022-09-29 12:39:20 -05:00
Peter Rotich
9ed39d9c95 Merge pull request #6284 from protich/issue/manage-users-password-policy
Reviewed-By: osTicket Devs <devs@osticket.com>
2022-09-29 12:38:37 -05:00
Peter Rotich
06e334fa89 Merge pull request #6283 from JediKev/issue/user-agent-password-reset
Reviewed-By: osTicket Devs <devs@osticket.com>
2022-09-29 12:38:15 -05:00
Peter Rotich
11c3b2e2b4 Merge pull request #6282 from JediKev/issue/imagesx-boolean
Reviewed-By: osTicket Devs <devs@osticket.com>
2022-09-29 12:35:08 -05:00
Peter Rotich
ce2b1ef3d4 Merge pull request #6281 from protich/issue/mailbox-exceptions
Reviewed-By: osTicket Devs <devs@osticket.com>
2022-09-29 12:34:44 -05:00
Peter Rotich
1f0a5ed086 Merge pull request #6279 from protich/issue/form-notice
Reviewed-By: osTicket Devs <devs@osticket.com>
2022-09-29 12:34:23 -05:00
Peter Rotich
08b7a18080 Merge pull request #6280 from protich/issue/misc-v116-fixes
Reviewed-By: osTicket Devs <devs@osticket.com>
2022-09-29 12:33:58 -05:00
Peter Rotich
423a0d74b1 Merge pull request #6278 from protich/issue/email-parse-error
Reviewed-By: osTicket Devs <devs@osticket.com>
2022-09-29 12:33:32 -05:00
Peter Rotich
39319e5b3d Merge branch '1.16.x' into 1.17.x 2022-09-29 17:25:45 +00:00
Peter Rotich
d98bbfcdfe Merge remote-tracking branch 'upstream/develop' into 1.16.x 2022-09-29 17:24:19 +00:00
Peter Rotich
d149ea0d42 v1.17 Misc. Fixes 2022-09-29 16:24:56 +00:00
Peter Rotich
21c71c6ed2 file: Possible Key Collision
This commit is a temporary fix for possible Fatal Crash when file key (hash
lookup) matches more than one object on lookup. See TODO note for more
information.
2022-09-28 22:45:28 +00:00
JediKev
5d06ee4618 issue: imagesx Boolean
This addresses an issue where if `imagecreatefromstring()` fails it
returns false (boolean) and we then pass that to `imagesx()`. This
method only accepts Gdimage (since PHP 8) so this adds a check to see if
`$image` can be set and if so we continue otherwise we skip the `if()`
completely and go straight to sending the data.
2022-09-28 21:36:06 +00:00
Peter Rotich
ebfc1d89f6 bug: Users Password Policy
Use the correct backend id on Users > Password Policy dropdown
2022-09-28 21:22:14 +00:00
JediKev
d3de159915 issue: User/Agent Password Reset
This addresses an issue where password reset is triggered even though the
user/agent doesn’t have a password or the authentication backend is
external.
2022-09-28 21:10:14 +00:00
Peter Rotich
81d5cb351b mail: Catch Possible Exceptions and Errors
This commit adds try/catch to MailBox related routines that shouldn't
necessarily affect email processing on failure.
2022-09-28 19:42:10 +00:00
Peter Rotich
4c0341e232 bug: Fix Sprintf Param placement 2022-09-28 17:40:15 +00:00
Peter Rotich
6e54e8106c forms: Add getNotice func. to DynamicFormEntry
This commit fixes a bug where getNotice was undefined when rendering a dynamic
form entry. It now delegates getting a notice, if any, from the form impl.
2022-09-28 17:33:31 +00:00
Peter Rotich
459520e8ed fetcher: Fail Safely on Email Parse Error
This commit adds ability to log email parse errors. This can happen if the
system fails to fetch complete raw email or when the email content is malformed
somehow.
2022-09-28 17:29:10 +00:00
Peter Rotich
e724ccb927 Merge pull request #6272 from protich/issue/ticket-denied
Reviewed-By: osTicket Team <devs@osticket.com>
2022-09-27 16:41:15 -05:00
Peter Rotich
5c5bd41e37 Merge pull request #6276 from protich/issue/filestoragebackend-errors
Reviewed-By: osTicket Team <devs@osticket.com>
2022-09-27 16:40:59 -05:00
Peter Rotich
1a47825aa6 Merge pull request #6271 from protich/issue/mailer-from-address
Reviewed-By: osTicket Team <devs@osticket.com>
2022-09-27 16:40:45 -05:00
Peter Rotich
b67f0a6e53 Merge pull request #6270 from protich/issue/attachments
Reviewed-By: osTicket Team <devs@osticket.com>
2022-09-27 16:40:26 -05:00
Peter Rotich
e8959f6af3 Merge pull request #6277 from aydreeihn/issue/plugin-inst-blacklist
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-27 16:38:50 -05:00
Peter Rotich
be178da5ca fetcher: Log Last Fetch on attempt
Log last fetch timestamp on login and before processing emails so that the
mailbox can yield to other mailboxes in the fetch queue.

Also catch Throwables instead of just Exceptions
2022-09-27 21:22:00 +00:00
Peter Rotich
e027dcc433 fetcher: Fetch Order based on Last Activity
Sort MailBoxes to fetch based on the last activity in ASC order. This is
necessary to give every mailbox a chance to be fetched.
2022-09-27 21:22:00 +00:00
Peter Rotich
79209144d1 fetcher: Catch Throwable
Catch Generic Exceptions / Errors
2022-09-27 21:21:07 +00:00
Peter Rotich
e685636ebc files: Catch possible Storage Backends Errors
Catch possible throwables (Exceptions and Errors) from file storage backends.
This is necessary so we don't crash while trying to save a file when storage
backend is having issues or bugs. osTicket uses database as fallback storage.
2022-09-27 21:07:20 +00:00
Peter Rotich
6932ea84cf Merge remote-tracking branch 'upstream/develop' into develop 2022-09-27 20:54:54 +00:00
aydreeihn
1ba8d37afc Issue: File Storage Plugins Blacklist
This commit will add the file storage plugins to the multi instance blacklist until we update them to offer multi instance support.
2022-09-27 14:47:28 -05:00
Peter Rotich
58f64e65b0 fetcher: TicketDenied Exception
Address a bug on how TicketDenied Exception is handled
2022-09-27 00:59:02 +00:00
Peter Rotich
75605a3fe9 mailer: Reset FROM Address when SMTP fails
This commit adds ability to reset FROM address when SMTP attempt fails. This is
important becacause of Spoofing Allowed setting.
2022-09-27 00:54:48 +00:00
Peter Rotich
2218aaca75 mailer: Skip Missing Attachment Files
This commit addresses an issue where mailer would crash due to missing
attachment files when sending out emails.
2022-09-26 14:25:25 +00:00
Peter Rotich
557d79f921 Merge pull request #6268 from ChrisTG742/patch-2
Reviwed-By: Peter Rotich <peter@osticket.com>
2022-09-25 23:12:53 -05:00
Chris
12a4cb093b Fix for https://github.com/osTicket/osTicket/issues/6267
Fix for Issue https://github.com/osTicket/osTicket/issues/6267, the order of the vars in line 691 was wrong.
2022-09-24 14:08:39 +02:00
JediKev
40d6ecbdd7 Merge branch 'develop' into 1.17.x 2022-09-23 18:59:18 +00:00
JediKev
edc7993289 Merge pull request #6262 from protich/issue/mailer-invalid-recipients
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>, protich <peter@osticket.com>
2022-09-23 13:49:51 -05:00
Peter Rotich
d7d37c4cce Mailer: Change FromAddress to EmailAddress object + Spoofing
This commit refactors how FROM address is set and used within the Mailer. It's
now set as EmailAddress object - making it easy to get Email and Name
separately.

We're also removing Spoofing Check when determining possible SMTP accounts to
use for outgoing emails. It's now checked while attempting to send a Message.
If the account in question doesn't allow Spoofing then the FROM header is reset
to the accounts email address.
2022-09-23 07:37:56 +00:00
Peter Rotich
551f786464 email: EmailAddress:__toString()
This commit partially revers 5d799ecd to return Email when EmailAddress is
casted to String instead of Email Address (email+name)
2022-09-23 06:53:27 +00:00
Peter Rotich
046f432765 mailer: Skip Invalid Email Recipients
This commit adds ability for Mailer to skip invalid email recipients when
sending out an email. A Mailer warning with the recipient info (name & email
address) is logged.

For uniformity sake EmailContact interface now requires getEmailAddress() to be
defined downstream. This was necessary because some of the objects (e.g Staff)
return a name when casted to string (__toString) and changing it will affect
variables used in email templates and possibly leak the agents email.
2022-09-23 06:47:44 +00:00
Peter Rotich
b1e5386491 Merge pull request #6261 from JediKev/issue/undefined-var-orm
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-22 14:20:35 -05:00
JediKev
4009881ad8 issue: Undefined Variable ORM
This addresses an issue where a wrong variable was used which caused
undefined variable errors with the built-in tests. This corrects the
variable from `$sql` to `$exec->sql`.
2022-09-22 13:39:30 -05:00
JediKev
f8e6c327f1 Merge pull request #6258 from JediKev/issue/session-cookie-refresh
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>, protich <peter@osticket.com>
2022-09-22 12:36:21 -05:00
JediKev
1630459a21 issue: Session Cookie Refresh
This addresses an issue where we were not renewing the cookie expired
time correctly when refreshing the session. This updates
`refreshSession()` to accept a `$refreshRate` defaulting to 60
(seconds). This also updates the `if()` statement from a negative to a
postive logic for better readability.
2022-09-22 12:19:31 -05:00
JediKev
d4ec4dd143 Merge pull request #6260 from protich/issue/plugin-instances
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>, protich <peter@osticket.com>
2022-09-22 09:58:09 -05:00
JediKev
3a486600b4 Merge pull request #6259 from protich/issue/plugins-version
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>, protich <peter@osticket.com>
2022-09-22 09:54:47 -05:00
JediKev
250f4195d1 Merge pull request #6257 from JediKev/issue/fix-upgrader-session-issues
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>, protich <peter@osticket.com>
2022-09-22 09:47:12 -05:00
JediKev
9b3cd60bed Merge pull request #6256 from protich/1.17.x
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>, protich <peter@osticket.com>
2022-09-22 09:43:09 -05:00
Peter Rotich
5607751671 plugins: Multi-Instance Blacklist
Some Plugins DO NOT or SHOULDN'T support multiple instances due design issues
or simply because of the fact that it doesn't make sense to do so.

The blacklist will be removed down the road once plugins are forced to declare
compatiple core osTicket version and if it supports multiple instances.
2022-09-22 02:20:51 +00:00
Peter Rotich
80bcc191d1 plugins: Make Plugins Upgradeable Again!
This adds ability and interface to auto-trigger plugins upgrade or downgrade
when there's a version mismatch on-load.

The base plugin class simply updates the version (for now) - the downstream
Plugins should implement the actual upgrade / downgrade process as needed,
especially if it creates database tables or needs to change stored config info
to match the new version format.
2022-09-22 01:47:09 +00:00
JediKev
311089a62e issue: Fix Upgrader Session Issues
This addresses session issues when upgrading from versions pre-1.7. This
bypasses the DBSessionBackend if `MAJOR_VERSION` is not defined or an
upgrade is pending and the `getDBVersion()` method returns `true` to
default to using the local session storage.
2022-09-21 15:55:15 -05:00
Peter Rotich
21e3a407dd smtp: Fix Crash on SMTP Error
This commit addresses a fatal error when system tries to log an error on SMTP account exception.
2022-09-21 20:32:59 +00:00
Peter Rotich
f3446599ea Merge pull request #6253 from ChrisTG742/patch-1
Reviewed-By: <peter@osticket.com>
2022-09-21 13:03:28 -05:00
Peter Rotich
5d799ecdf4 email: Add getEmail to EmailAddress
This commits adds getEmail to EmailAddress class to provide an interface for
osTicket Mailer to get the email address without the name.
2022-09-21 17:33:19 +00:00
Peter Rotich
6c2ecad220 mail: Fix Namespace Type 2022-09-21 16:52:32 +00:00
Peter Rotich
f10de7af90 Merge pull request #6254 from protich/issue/email_encoding
email: Set Default Email Message Encoding to UTF-8
2022-09-20 19:05:03 -05:00
Peter Rotich
f07d2c07b9 Merge pull request #6251 from protich/issue/mailfetch
Mail Fetcher Fixes
2022-09-20 19:04:50 -05:00
Peter Rotich
8954f05b6a mailer: Make Laminas Mail Happy Again
This commits split email and name to avoid the need for Laminas Mail to parse
the email address parts from a string - saving a couple of CPU cycles.
2022-09-20 14:31:28 +00:00
Peter Rotich
4656cf4cbc email: Set Default Email Message Encoding to UTF-8
This commits sets UTF-8 as the default encoding. It's necessary because - by
default Laminas Mail uses ASCII encoding - which can cause `invalid header
value` exception when an email address of the sender or a recipient has an
accented character.

Please note that individual Mine Parts set thier own encoding as needed - by
default osTicket uses base64 to encode `text/{plain/html}` parts to reduce the
overrall size of the Message - configuratable option to make `quoted-printable`
encoding an option is planned.
2022-09-19 23:49:10 +00:00
Peter Rotich
3decbe7863 Mail Fetcher: Handle Denied Tickets
This commit addresses a bug where the new Mail Fetcher failed to handle Denied
Tickets (via banlist/filter) correctly. In addition, email parsing is now
delagated to Mail Parser utility class.
2022-09-19 17:58:39 +00:00
Peter Rotich
1eebcd6661 email: Get Host Info from Email Account Setting
This commit addresees an issue where mail fetcher crashed when logging a
warning on excessive fetch errors. As part of the fix, Email AccountSetting is
now the single source of truth when it comes to Email Account information.
2022-09-19 17:58:39 +00:00
Chris
d5545b4e82 Update class.mail.php
Fix for typo in function auth(): expect need to be set to 334 instead of 234 here.
2022-09-19 13:29:54 +02:00
JediKev
a1a3d450b7 Merge branch 'develop' into 1.17.x 2022-09-15 19:31:29 +00:00
JediKev
ec331a549f Merge branch 'develop' into 1.16.x 2022-09-15 19:22:07 +00:00
JediKev
365a57ea3c Merge branch '1.15.x' into develop 2022-09-15 19:20:27 +00:00
Peter Rotich
0f81f5f3ac Merge pull request #6176 from JediKev/oops/jquery-ui-images
Reviewed-By:  Peter Rotich <peter@osticket.com>
2022-09-13 08:39:29 -05:00
JediKev
356e156ab2 Add Release Notes for v1.17-rc4 2022-09-12 19:53:59 +00:00
JediKev
6607facb6e Merge pull request #6249 from protich/issue/mail
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-09-12 14:48:13 -05:00
JediKev
1247609af9 Merge pull request #6246 from protich/1.17.x
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-09-12 14:45:48 -05:00
Peter Rotich
c71ddade63 mailer: Dropped Attachments
This commit fixes a bug that caused attachments to be dropped
2022-09-12 17:49:45 +00:00
Peter Rotich
2318714e25 plugin: Disable Multi-intance Support for 2FA plugin
Official 2FA Plugin  doesn't support multiple instances at the moment due to how the
plugin loads config info for versions <= 0.3
2022-09-12 03:11:14 +00:00
Peter Rotich
6de79a0bd4 mail: Mixed Mime Message with mixed multiparts
This commit addresses nesting issue when a message is mixed (with inline images
or attachments) and also has alternate content (plain & html).
2022-09-11 02:59:38 +00:00
Peter Rotich
f8f5d3dd7d 2fa: Fix 2FA Backend Registry
This commit updates 2FA Backend registery to match v1.17.x model
2022-09-08 21:09:39 +00:00
Peter Rotich
abbe1d2d99 Merge pull request #6230 from protich/feature/oauth2
Reviewed-By: osTicket Devs <devs@osticket.com>
2022-09-08 16:03:25 -05:00
Peter Rotich
c6ac4e7452 Merge pull request #6245 from JediKev/issue/dispatcher-include_once
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-08 16:02:44 -05:00
JediKev
dab490dc27 issue: Dispatcher include_once
This addresses issue 6218 where using `include` for things like
`api.tickets.php` fails with fatal error of `Cannot declare class xxx,
becuase the name is already in use`. This changes `include` to
`include_once` so that we avoid fatal errors and the files still get
included.
2022-09-08 11:20:42 -05:00
Peter Rotich
09197da959 Merge pull request #6241 from JediKev/php/8.1-support
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-08 11:19:53 -05:00
JediKev
dd2b9a4309 fix: Upgrader Issues
This addresses some various Upgrader issues when attempting to upgrade
from an older version (eg. `v1.6` and `v1.7`). This includes catching
more exception types, making a method static, adding some variable
checks before calling `count()`, checking for session ID before setting
custom handlers in the session class, and last (but not least) a
try-catch statement around `$__db->query()` calls so that we can catch
Excpetions without exiting.
2022-09-08 11:16:02 -05:00
Peter Rotich
eed1b66884 Merge pull request #6232 from jasplin/fix/storage-plugin-unhandledexception
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-08 10:59:35 -05:00
Peter Rotich
32877dcac0 Merge pull request #6243 from aydreeihn/issue/installer-yaml
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-08 10:58:12 -05:00
Peter Rotich
1388829286 email: Rename AccountOptions to AccountSetting
Options is a little confusing in the context used.
2022-09-08 08:26:36 +00:00
Peter Rotich
7123403e4f Merge pull request #6240 from JediKev/issue/searching-numbers
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-08 02:43:52 -05:00
Peter Rotich
774771cd8e Merge pull request #6235 from JediKev/issue/staff-ticket-open-session
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-08 02:43:07 -05:00
Peter Rotich
8f88a5513a Merge pull request #6239 from JediKev/issue/external-auth-regenerate-session
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-08 02:42:01 -05:00
Peter Rotich
5b76b887ac Merge pull request #6238 from JediKev/issue/unique-variable-names
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-08 02:40:46 -05:00
Peter Rotich
42e7b713f3 Merge pull request #6216 from JediKev/mailparse/thread-entry-recipients-to
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-08 02:39:05 -05:00
Peter Rotich
5717bdd427 Merge pull request #6233 from JediKev/issue/auth-backend-getBkId
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-08 02:38:31 -05:00
Peter Rotich
9f2ea7b0f9 Merge pull request #6228 from JediKev/installer/checking-existing-install
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-08 02:37:26 -05:00
Peter Rotich
9cb4408db5 Merge pull request #6234 from JediKev/plugins/audit-enable
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-08 02:36:39 -05:00
Peter Rotich
8e8d4f4fe1 Merge pull request #6242 from Wyrrrd/reflection
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-09-08 02:35:34 -05:00
Peter Rotich
2fa077aec7 plugins: Plugin Instance Config Class
This commit adds additional enhancements to allow Instances to dynamically set
Config Class. See e4f594c
2022-09-08 07:26:25 +00:00
Peter Rotich
481a047576 email: Validate Basic Auth Credentials
This commit adds validating when settting username and password.
2022-09-08 06:01:36 +00:00
JediKev
c24ee5753e issue: Fix Deprecation Warnings (line_endings)
This fixes the Deprecation warnings when attempting to set INI directive
`ini_set('auto_detect_line_endings', true)`. This directive is no longer
supported so this removes the calls completely.
2022-09-07 10:08:28 -05:00
JediKev
22b599e314 issue: Fix Deprecation Warnings (Serializable)
This addresses the Deprecation warnings when attempting to implement the
Serializable interface without defining the `__serialize()` and
`__unserialize()` methods. This updates the code for each class that
implements Serializable to define these methods and to call the original
methods we have.
2022-09-07 10:08:21 -05:00
Peter Rotich
5e53b1a131 issue: PasswordPolicyBackend
Use plugin getBkId() instead of getId() inline with plugins multi-instance
support.
2022-09-06 16:12:33 +00:00
aydreeihn
6780f29e74 Issue: Installer YAML
This commit fixes an issue where some default data was not being inserted into the database for fresh installs:

1. Sales Department
	The Sales Department was not being created because we were trying to set an sla_id before the SLA table was created.

2. Banlist
	The default Filter Rule and Action for the System Banlist was not being created because the save function now calls the ORM save function, so it is no longer calling save_rules and save_actions for the installer. We also have to change the YAML to refer to the fields as w,h,v instead of what, how, value for them to be recognized correctly.

It also fixes some spacing issues in the Filter class.
2022-09-06 09:51:53 -05:00
Peter Rotich
a706866844 plugins: Fix PluginConfig config load bug
This commit addresses an issue where Password Policy Plugin crashed on login
when password expiration is set to "Never Expire" and php mode set to strict.

This is because v1.17 changed how plugin instances config values are stored to match
the actual field format (using to_db), but on load - PluginConfig mistakenly
defaulted to database value when to_php returned an empty value - which is
indeed correct value when selection was empty option.
2022-09-03 01:10:44 +00:00
Peter Rotich
4ee44d4830 plugin: Fix message typo on instance update 2022-09-02 17:17:11 +00:00
Peter Rotich
76bf772568 email: Check if global error is set already on update failure 2022-09-02 16:59:43 +00:00
Peter Rotich
e4f594c2d3 plugins: Allow Plugin Instances to set Config Form
This is necessary to allow plugin instances to use different config forms as
desired depending on internal logic. For example OAuth2 plugin can have both
Authentication and Authorization instances which require slightly diffrent
fields and validation.
2022-09-02 16:50:24 +00:00
Peter Rotich
246c144513 basic-auth: Use updated PasswordField options
This commit refactors password filed in Basic Authentication form to utilize
the PasswordField correctly.
2022-09-02 16:47:18 +00:00
Peter Rotich
bc154434dd forms: Add ability to disable ChoiceField fields 2022-09-02 16:45:57 +00:00
Peter Rotich
681bea0839 forms: Add ability to add notice (warning) to forms
This commit adds ability to inject and display a warning message on dynamically
rendered forms. This is helpful when various components want to add such
warnings while not having full control of the display logic.
2022-09-02 16:38:03 +00:00
Wyrrrd
55f61390cc Replace hacky way with ReflectionClass 2022-09-01 14:37:56 +00:00
Peter Rotich
e705ff0f6c email: Add ability to stash / restore data
his is adds ability to stash data before doing external redirect when
configuring External Authorization e.g OAuth2
2022-08-29 21:35:53 +00:00
JediKev
e00003f68a issue: Searching Numbers
This updates the ticket search mechanism to check if the supplied keyword is
a number. If the keyword is a number we will only search Ticket Numbers to
improve search performance.
2022-08-29 15:22:56 +00:00
Peter Rotich
3024d7f50c smtp: Fix Configure Mailbox Authentication Error
This commit addreses an error that happens when "Same as Mailbox"
authentication is selected when configuring SMTP Account.
2022-08-29 05:35:54 +00:00
JediKev
8325226046 issue: External Auth Regenerate Session
This addresses an issue where we are rotating the session on each login
to help prevent session fixation. However, this causes an issue when
using an External Authentication Backend such as OAuth2. This remove the
`regenerateSession()` calls from the `login()` methods and instead we
will set a time bomb that will auto regenerate the session a certain
amount of time after login. This way external auth backends will still
function properly and we can still regenerate the session.
2022-08-26 11:38:44 -05:00
JediKev
0dc5a02e4a issue: Unique Variable Names
This addresses an issue where we are not checking the Field Variable
Names with case-insensitive comparison. This causes issues as the
database doesn't care about case and sees the two variables as the same.
This hardens our variable name checks to convert existing names and the
new name(s) to lowercase for case-insensitve comparison. With this
change it will no longer allow duplicate variable names (with a
difference of case).
2022-08-26 11:31:35 -05:00
JediKev
7796d60c1d plugins: Audit enable()
This addresses an issue where the Audit Plugin relies on `enable()`
being called in order to create the `_audit` table required for saving
audit data to the database. This adds the method back temporarily until
we can rewrite the plugin to fit the new standards. This also attempts
to call `enable()` on install so that the table can be created. If the
plugin doesn't have an `enable()` method it defaults to the parent class
Plugin `enable()` which just returns `true`.
2022-08-25 13:59:13 -05:00
JediKev
4f3d3cff7c issue: Staff Ticket Open Session
This addresses an issue where we are not regenerating the session for
Staff members after they've opened a Ticket. This adds
`$thisstaff->regenerateSession()` after creating a Ticket so that the
session is regenerated.
2022-08-25 13:27:31 -05:00
JediKev
29f01c090a issue: AuthenticationBackend getBkId()
This addresses an issue where we are calling `getId()` instead of
`getBkId()` in some places when registering and listing plugin instances. This causes
an issue if a User is auto-created via something like LDAP and the
`backend` is set to something like `ldap.client.p[id]i[id]`. When we get
available/registered backends we are not selecting the appropriate ID so
the backend is considered "not in the list" and does not let the User
authenticate. This changes all the calls to `getBkId()` to ensure we are
selecting the appropriate ID.
2022-08-25 10:42:25 -05:00
John Asplin
ca5932ac6f add try/catch to get inline email image 2022-08-23 17:42:49 +01:00
Peter Rotich
ab9a810181 email: Add routine to log fetch errors 2022-08-23 00:50:56 +00:00
Peter Rotich
bd300c44d3 Basic Auth: Remove Maxlength
This commit removes maxlength restriction of 30 added upstream by default to TextboxField.
2022-08-22 15:35:04 +00:00
JediKev
a3f2e24647 installer: Checking Existing Install
This addresses an issue with PHP 8.1 where on install we run a query to
see if we can select data from the ost_config table to see if there is
already an instance installed to avoid overwriting data. In PHP 8.1 this
will throw a fatal error which kills the installer. This adds a `try {}
catch() {}` statement around the call so that if it does throw an error
it continues without issues. This is what we want anyways, if the query
returns false or an error then we want to continue.
2022-08-19 10:54:03 -05:00
JediKev
34c64f14b4 Add Release Notes for v1.17-rc3 2022-08-05 14:56:56 +00:00
JediKev
e4db87635f Merge pull request #6217 from protich/feature/oauth2
Reviewed-By: JediKev <kevin@enhancesoft.com>, aydreeihn <adriane@enhancesoft.com>
2022-08-05 09:49:50 -05:00
Peter Rotich
fdc5b0fa82 oauth2: Verify Config Signature
This commit adds ability to verify config signature when getting Access Token.
2022-08-04 21:04:10 +00:00
Peter Rotich
d9b41f4e5c plugin: Plugin Instance Config Signature
This commit addresses a bug where config signature routine used ksort
incorrectly - resulting in same signature everytime.
2022-08-04 21:04:10 +00:00
Peter Rotich
fa8dc24d0e email: OAuth2 Backend Change
Update OAuth2 backend on update since it could be changed on instance update.
2022-08-04 21:04:04 +00:00
Peter Rotich
265065c1d9 email: Port Password to the new Encryption Scheme 2022-08-04 19:13:30 +00:00
JediKev
a82af69845 mailparse: thread_entry_recipients to
This addresses an issue where some emails don't have a To header which
causes a fatal error when parsing mail. This is due to assuming To is set
and passing it to array_unique which expects the value to be an array. This
adds a check to see if To is set and if so we call array_unique otherwise we
default to an empty array.
2022-08-04 19:13:24 +00:00
Peter Rotich
4576a6ef07 Merge pull request #6200 from JediKev/issue/manually-sorted-topics
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-08-01 13:15:21 -05:00
Peter Rotich
55f3dd3c04 Merge pull request #6199 from JediKev/i18n/default-ticket-queue
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-08-01 13:14:14 -05:00
Peter Rotich
aef471b10a Merge pull request #6214 from JediKev/issue/queue-export-headings
Reviewed-By: Peter Rotich <peter@osticket.com>
2022-08-01 13:12:41 -05:00
JediKev
4744975840 issue: Queue Export Headings
This addresses an issue where updating the Queue Export Column Headings
doesn't actually save the changes. This is due to using the previous heading
regardless of the headings being passed in. This adds a check for the column
heading in `$fields` and if exists we use that otherwise we default back to
the previous heading.
2022-07-27 21:24:10 +00:00
JediKev
a80db8a7f7 issue: Manually Sorted Topics
This addresses issue 6197 where setting Help Topics sort to Manually throws
a fatal error. This is due to not checking if `$_POST['ids']` is an array
before passing it to `array_intersect()`. This adds a check to see if the
ids are an array and if not then `$diff` will be set to an empty array
(which is what `array_intersect()` returns if the arrays have no common
values).
2022-07-11 17:36:23 +00:00
JediKev
7a5dd0ebeb i18n: Default Ticket Queue
This adds the Help Tip for Default Ticket Queue setting in Admin Panel >
Settings > Tickets > Settings.
2022-07-11 16:00:30 +00:00
JediKev
822a05fedb oops: jQuery UI Images
This adds the images missed in the jQuery UI upgrade. This addresses an
issue where the date picker doesn't show the arrows to skip months/years.
2022-06-01 17:01:41 +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
3150 changed files with 75735 additions and 232478 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,274 @@
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
* email: Set Default Email Message Encoding to UTF-8 (4656cf4c, 8954f05b)
* plugins: Multi-Instance Blacklist (56077516)
* mailer: Reset FROM Address when SMTP fails (75605a3f)
* Issue: File Storage Plugins Blacklist (1ba8d37a)
* mail: Catch Possible Exceptions and Errors (81d5cb35)
### Improvements
* Update class.mail.php (d5545b4e)
* Mail Fetcher Fixes (1eebcd66, 3decbe78)
* 1.17.x Bug Fixes (6c2ecad2, 5d799ecd, 21e3a407)
* plugins: Make Plugins Upgradeable Again! (80bcc191)
* mailer: Skip Invalid Email Recipients (046f4327, 551f7864, d7d37c4c)
* issue: Wrong Variable Order (12a4cb09)
* mailer: Skip Missing Attachment Files (2218aaca)
* fetcher: TicketDenied Exception + Throwable (58f64e65, 79209144, e027dcc4, be178da5)
* files: Catch possible Storage Backends Errors (e685636e)
* fetcher: Fail Safely on Email Parse Error (459520e8)
* forms: Add getNotice func. to DynamicFormEntry (6e54e810)
* bug: Users Password Policy (ebfc1d89)
* v1.17 Misc. Fixes (d149ea0d)
* plugins: Make Plugin Base Class Play Nice (7b26f665, 5787f9ae, 3163972b)
osTicket v1.17-rc4
==================
### Enhancements
* v1.17: Bug Fixes & Enhancements (bd300c44, ab9a8101, 3024d7f5, e705ff0f, 681bea08, bc154434, 246c1445, e4f594c2, 76bf7725, 4ee44d48, a7068668, 5e53b1a1, 481a0475, 2fa077ae, 13888292)
### Improvements
* mail: Mixed Mime Message with mixed multiparts (6de79a0b, c71ddade)
* 2fa: Fix 2FA Backend Registry (f8f5d3dd, 2318714e)
* issue: Dispatcher include_once (dab490dc)
* plugins: Audit enable() (7796d60c)
* issue: AuthenticationBackend getBkId() (29f01c09)
osTicket v1.17-rc3
==================
### Improvements
* Oauth2 / Modern Email Auth Latest Fixes (fdc5b0fa, d9b41f4e, fa8dc24d, 265065c1)
osTicket v1.17-rc2
==================
### Improvements
@@ -8,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.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.5 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.2 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,61 +112,84 @@ 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)
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();
}
if (!$data)
$data = $this->getEmailRequest();
function processEmail($data=false, array $defaults = []) {
try {
if (!$data)
$data = $this->getEmailRequest();
elseif (!is_array($data))
$data = $this->parseEmail($data);
} catch (Exception $ex) {
throw new EmailParseError($ex->getMessage());
}
$data = array_merge($defaults, $data);
$seen = false;
if (($entry = ThreadEntry::lookupByEmailHeaders($data, $seen))
&& ($message = $entry->postEmail($data))
@@ -183,18 +213,34 @@ class TicketApiController extends ApiController {
// All emails which do not appear to be part of an existing thread
// will always create new "Tickets". All other objects will need to
// be created via the web interface or the API
return $this->createTicket($data);
try {
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;
@@ -219,19 +265,33 @@ 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()))
return $pipe->response(201, $ticket->getNumber());
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 {}
class EmailParseError extends Exception {}
?>

View File

@@ -112,11 +112,11 @@ abstract class TwoFactorAuthenticationBackend extends ServiceRegistry {
|| !($class instanceof TwoFactorAuthenticationBackend))
return false;
static::$registry[$class->getId()] = $class;
static::$registry[$class->getBkId()] = $class;
}
static function allRegistered() {
return self::getRegistry();
return array_merge(self::$registry, parent::getRegistry());
}
static function getBackend($id) {
@@ -140,13 +140,18 @@ class ExpiredOTP extends Exception {}
*/
abstract class Staff2FABackend extends TwoFactorAuthenticationBackend {
static protected $registry = array();
static function allRegistered() {
return array_merge(self::$registry, parent::allRegistered());
}
abstract function send($user);
abstract function validate($form, $user);
}
abstract class User2FABackend extends TwoFactorAuthenticationBackend {
static protected $registry = array();
static function allRegistered() {
return array_merge(self::$registry, parent::allRegistered());
}
abstract function send($user);
abstract function validate($form, $user);
}

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,20 +219,28 @@ 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) {
global $ost;
$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, $validate);
}
function getEmailRequest() {
if (!($data=$this->getRequest('email', false)))
$this->exerr(400, __("Unable to read email request"));
return $data;
}
function parseRequest($stream, $format, $validate=true) {
$parser = null;
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':
@@ -219,23 +250,24 @@ class ApiController extends Controller {
$parser = new ApiEmailDataParser();
break;
default:
$this->exerr(415, __('Unsupported data format'));
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());
}
//Validate structure of the request.
$this->validate($data, $format, false);
if ($validate && $data)
$this->validate($data, $format, false);
return $data;
}
function getEmailRequest() {
return $this->getRequest('email');
function parseEmail($content) {
return $this->parseRequest($content, 'email', false);
}
/**
* Structure to validate the request against -- must be overridden to be
* useful
@@ -259,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;
}
@@ -282,37 +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') {
fwrite(STDERR, "({$code}) $error\n");
}
else {
$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);
}
}
@@ -329,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;
@@ -436,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

@@ -178,7 +178,7 @@ abstract class OAuth2Backend extends ServiceRegistry {
|| !($bk instanceof OAuth2Backend))
return false;
static::$registry[$bk->getId()] = $bk;
static::$registry[$bk->getBkId()] = $bk;
}
static function allRegistered() {
@@ -330,7 +330,7 @@ abstract class AuthenticationBackend extends ServiceRegistry {
foreach (static::allRegistered() as $bk) {
if ($backends //Allowed backends
&& $bk->supportsInteractiveAuthentication()
&& !in_array($bk->getId(), $backends))
&& !in_array($bk->getBkId(), $backends))
// User cannot be authenticated against this backend
continue;
@@ -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) {
@@ -589,7 +589,7 @@ abstract class StaffAuthenticationBackend extends AuthenticationBackend {
if (!($backends=self::getAllowedBackends($staff->getId())))
return true; //No restrictions
return in_array($bk->getId(), array_map('strtolower', $backends));
return in_array($bk->getBkId(), array_map('strtolower', $backends));
}
function getPasswordPolicies($user=null) {
@@ -597,7 +597,7 @@ abstract class StaffAuthenticationBackend extends AuthenticationBackend {
$policies = PasswordPolicy::allActivePolicies();
if ($cfg && ($policy = $cfg->getStaffPasswordPolicy())) {
foreach ($policies as $P)
if ($policy == $P->getId())
if ($policy == $P->getBkId())
return array($P);
}
@@ -633,8 +633,6 @@ abstract class StaffAuthenticationBackend extends AuthenticationBackend {
|| !($authkey=$bk->getAuthKey($staff)))
return false;
// Regenerate session id and refresh (avoid session fixation)
$staff->regenerateSession();
//Log debug info.
$ost->logDebug(_S('Agent Login'),
sprintf(_S("%s logged in [%s], via %s"), $staff->getUserName(),
@@ -660,6 +658,8 @@ abstract class StaffAuthenticationBackend extends AuthenticationBackend {
$authsession['id'] = $staff->getId();
$authsession['key'] = $authkey;
$authsession['2fa'] = $auth2fa;
// Set TIME_BOMB to regenerate the session 10 seconds after login
$_SESSION['TIME_BOMB'] = time() + 10;
// Set session token
$staff->setSessionToken();
// Set Auth Key
@@ -850,8 +850,6 @@ abstract class UserAuthenticationBackend extends AuthenticationBackend {
throw new AccessDenied(__('Account is administratively locked'));
}
// Regenerate session id and refresh (avoid session fixation)
$user->regenerateSession();
// Tag the user and associated ticket in the SESSION
$this->setAuthKey($user, $bk, $authkey);
// Set Session Token
@@ -869,6 +867,9 @@ abstract class UserAuthenticationBackend extends AuthenticationBackend {
$type = array('type' => 'login');
Signal::send('person.login', $u, $type);
// Set TIME_BOMB to regenerate the session 10 seconds after login
$_SESSION['TIME_BOMB'] = time() + 10;
if ($bk->supportsInteractiveAuthentication() && ($acct=$user->getAccount()))
$acct->cancelResetTokens();
@@ -1254,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');
@@ -1481,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');
@@ -1593,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'));
@@ -1621,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

@@ -151,6 +151,19 @@ implements EmailContact, ITicketUser, TemplateVariable {
return $this->user->getId();
}
function getEmailAddress() {
$emailaddr = (string) $this->getEmail();
if (($name=$this->getName()))
$emailaddr = sprintf('"%s" <%s>',
(string) $name,
$emailaddr);
return $emailaddr;
}
function __toString() {
return $this->getEmailAddress();
}
abstract function getTicketId();
abstract function getTicket();
}
@@ -484,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

@@ -39,12 +39,6 @@ implements EmailContact, ITicketUser {
var $active;
function __toString() {
return Format::htmlchars($this->toString());
}
function toString() {
return sprintf('"%s" <%s>', $this->getName(), $this->getEmail());
}
function getId() {
return $this->id;
@@ -81,6 +75,16 @@ implements EmailContact, ITicketUser {
function getEmail() {
return $this->user->getEmail();
}
function getEmailAddress() {
$emailaddr = (string) $this->getEmail();
if (($name=$this->getName()))
$emailaddr = sprintf('"%s" <%s>',
(string) $name,
$emailaddr);
return $emailaddr;
}
function getName() {
return $this->user->getName();
}
@@ -135,6 +139,10 @@ implements EmailContact, ITicketUser {
return $this->user_id;
}
function __toString() {
return Format::htmlchars($this->getEmailAddress());
}
function hasFlag($flag) {
return ($this->get('flags', 0) & $flag) != 0;
}

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);
@@ -166,7 +170,7 @@ class UrlMatcher {
if (strpos($class, ":")) {
list($file, $class) = explode(":", $class, 2);
include $file;
include_once $file;
}
return array($class, $func);
}

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');
@@ -1034,6 +1034,10 @@ class DynamicFormEntry extends VerySimpleModel {
return $this->form->getInstructions();
}
function getNotice() {
return $this->form->getNotice();
}
function getDynamicForm() {
return $this->form;
}
@@ -1211,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(
@@ -1351,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();
@@ -1589,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) {
@@ -1629,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];
}
@@ -1729,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) {
@@ -1912,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');
@@ -73,17 +74,42 @@ class Email extends VerySimpleModel {
return $this->getAddress();
}
function stash($key, $data) {
// TODO: move stash/restore to a StashableTrait
function restore($key, $drop=true) {
if (($data = $this->stash($key, null)) && $drop)
$this->stash[$key] = null;
return $data;
}
function stash($key, $data=null) {
if (!isset($this->stash))
$this->stash = &$_SESSION[':email'][$this->getId()];
$this->stash[$key] = $data;
// If data is null then stash is being pop-ed
if (!isset($data) && $key && isset($this->stash[$key]))
return $this->stash[$key];
// stash data
if ($key && $data)
$this->stash[$key] = $data;
}
function stashFormData(array $data) {
$this->stash('formdata', array_filter($data));
}
function restoreFormData($drop=true) {
return $this->restore('formdata', $drop) ?: [];
}
function restoreErrors($drop=true) {
return $this->restore('errors', $drop) ?: [];
}
function restoreNotice($drop=true) {
return $this->restore('notice', $drop);
}
function getEmail() {
return $this->email;
}
@@ -165,6 +191,10 @@ class Email extends VerySimpleModel {
// SMTP Account Info
if (($smtp=$this->getSmtpAccount()))
$info = array_merge($info, $smtp->getInfo());
// Restore stahed formdata (if any)
if ($_SERVER['REQUEST_METHOD'] == 'GET'
&& ($data=$this->restoreFormData()))
$info = array_merge($info, $data);
return $info;
}
@@ -305,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())
@@ -393,9 +423,28 @@ class EmailAccount extends VerySimpleModel {
private $instance;
// If account supports tls or ssl
private $encryption = false;
// Account settings
private $settings;
public function getAccountOptions() {
return new osTicket\Mail\AccountOptions($this);
public function getAccountSetting($stashed=false) {
if (!isset($this->settings)) {
// Set properties to stashed form data (if any and requested)
if ($stashed && ($info=$this->getInfo())) {
foreach (['host', 'port', 'protocol'] as $p) {
$k = "{$this->type}_$p";
if (isset($info[$k]))
$this->{$p} = $info[$k];
}
}
$this->settings = new osTicket\Mail\AccountSetting($this);
}
return $this->settings;
}
public function getHostInfo() {
return $this->getAccountSetting()->getHostInfo();
}
public function getHost() {
@@ -406,8 +455,8 @@ class EmailAccount extends VerySimpleModel {
return $this->port;
}
public function getEncryption() {
return $this->encryption;
public function getProtocol() {
return $this->protocol;
}
public function getNumErrors() {
@@ -423,29 +472,48 @@ 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;
// No credentials stored or token expired (referesh failed)
if (!($cred=$this->getFreshCredentials())
|| !($token=$cred->getAccessToken())
|| $token->isExpired())
return true;
return (!($cred=$this->getFreshCredentials())
// Get token with signature match - mismatch means config
// changed somehow
|| !($token=$cred->getAccessToken($this->getConfigSignature()))
// Check if expired
|| $token->isExpired()
// If Strict Matching is enabled ensure the email matches
// the Resource Owner
|| !$this->checkStrictMatching($token));
// Signature mismatch - means config changed
return strcasecmp($token->getConfigSignature(),
$this->getConfigSignature());
}
public function getId() {
@@ -469,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;
}
@@ -484,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);
@@ -497,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()),
];
@@ -557,6 +636,11 @@ class EmailAccount extends VerySimpleModel {
if (isset($this->ht[$var]))
$ht[$this->type.'_'.$var] = $this->ht[$var];
}
// Add stashed info (if any)
if (($data=$this->email->restoreFormData(false)))
$ht = array_merge($ht, $data);
return $ht;
}
@@ -566,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;
@@ -582,8 +666,13 @@ class EmailAccount extends VerySimpleModel {
case 'basic':
$this->form = $this->getBasicAuthConfigForm($vars,
$auth);
$setting = $this->getAccountSetting(true);
if (!$setting || !$setting->isValid())
$this->form->setNotice(
__('Host, Port & Protocol Required'));
break;
}
}
return $this->form;
}
@@ -597,7 +686,8 @@ class EmailAccount extends VerySimpleModel {
switch ($type) {
case 'basic':
// Set username and password
if (!$vars || !$this->updateCredentials($auth, $vars, $errors))
if (!$this->updateCredentials($auth, $vars, $errors)
&& !isset($errors['err']))
$errors['err'] = sprintf('%s %s',
__('Error Saving'),
__('Authentication'));
@@ -617,10 +707,20 @@ class EmailAccount extends VerySimpleModel {
if ($this->getAuthId()
&& ($i=$bk->getPluginInstance($this->getAuthId()))) {
$vars = array_merge($bk->getDefaults(), $vars); #nolint
if (!$i->update($vars, $errors))
if ($i->update($vars, $errors)) {
// Disable account if backend is changed
if (strcasecmp($this->auth_bk, $auth))
$this->active = 0;
// 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'),
__('Authentication'));
__('Authentication'));
}
} else {
// Ask the backend to add OAuth2 instance for this account
if (($i=$bk->addPluginInstance($vars, $errors))) { #nolint
@@ -645,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)))
@@ -661,96 +765,206 @@ class EmailAccount extends VerySimpleModel {
// Authentication doesn't match - it's getting reconfigured.
if ($auth
&& strncasecmp($this->getAuthBk(), $auth, strlen($auth))
&& strcasecmp($auth, 'none'))
&& !in_array($auth, ['none', 'mailbox']))
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;
@@ -758,57 +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 (!$errors) {
$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',
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');
} else {
// 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',
@@ -819,17 +988,25 @@ 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;
}
public function logActivity($error= null, $now=null) {
public function logActivity($error=null, $now=null) {
if (isset($error)) {
$this->num_errors += 1;
$this->last_error_msg = $error;
@@ -837,9 +1014,10 @@ class EmailAccount extends VerySimpleModel {
} else {
$this->num_errors = 0;
$this->last_error = null;
$this->last_error_msg = null;
$this->last_activity = $now ?: SqlFunction::NOW();
}
$this->save();
return $this->save();
}
function save($refetch=false) {
@@ -851,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();
@@ -861,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;
}
@@ -895,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;
}
@@ -903,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() {
@@ -916,17 +1108,22 @@ class MailBoxAccount extends EmailAccount {
return $this->fetchmax;
}
public function getMailBox(osTicket\Mail\AuthCredentials $cred=null) {
protected function validateCredentials(osTicket\Mail\AuthCredentials $creds) {
return $this->getMailBox($creds);
}
public function getMailBox(?osTicket\Mail\AuthCredentials $cred=null) {
if (!isset($this->mailbox) || $cred) {
$this->cred = $cred ?: $this->getFreshCredentials();
$options = $this->getAccountOptions();
$setting = $this->getAccountSetting();
$setting->setCredentials($this->cred);
switch (strtolower($this->getProtocol())) {
case 'imap':
$mailbox = new osTicket\Mail\Imap($options);
$mailbox = new osTicket\Mail\Imap($setting);
break;
case 'pop3':
case 'pop':
$mailbox = new osTicket\Mail\Pop3($options);
$mailbox = new osTicket\Mail\Pop3($setting);
break;
default:
throw new Exception('Unknown Mail protocol:
@@ -939,14 +1136,15 @@ class MailBoxAccount extends EmailAccount {
public function fetchEmails() {
try {
$fetcher = new osTicket\Mail\Fetcher($this);
$num = $fetcher->processEmails();
$this->logLastFetch();
return $num;
} catch (Exception $ex) {
$this->logFetchError($ex->getMessage());
// rethrow the exception so caller can handle it
throw $ex;
$fetcher = new osTicket\Mail\Fetcher($this);
return $fetcher->processEmails();
} catch (Throwable $t) {
// May throw an Exception or Error
// Log the message
$this->logFetchError($t->getMessage());
// rethrow the throwable so caller can handle it
throw $t;
}
return 0;
}
@@ -1000,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']) {
@@ -1020,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'] =
@@ -1037,7 +1236,11 @@ class MailBoxAccount extends EmailAccount {
}
public function logLastFetch($now=null) {
return $this->logActivity($now);
return $this->logActivity(null, $now);
}
private function logFetchError($error) {
return $this->logActivity($error ?: __('Mail Fetch Error'));
}
public function update($vars, &$errors) {
@@ -1081,10 +1284,31 @@ 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);
}
protected function validateCredentials(osTicket\Mail\AuthCredentials $creds) {
return $this->getSmtp($creds);
}
public function getSmtpConnection() {
$this->smtp = $this->getSmtp();
if (!$this->smtp->connect())
@@ -1093,15 +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();
$accountOptions = $this->getAccountOptions();
$smtpOptions = new osTicket\Mail\SmtpOptions($accountOptions);
$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;
}
@@ -1127,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'];
@@ -1135,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) {
@@ -1171,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);
}
@@ -1181,40 +1431,45 @@ class EmailAccountConfig extends Config {
*
*/
class BasicAuthConfigForm extends AbstractForm {
function buildFields() {
$passwdhint = '';
if (isset($this->_source['passwd']) && $this->_source['passwd'])
$passwdhint = __('Enter a new password to change current one');
private $account;
private $host;
private $password;
function __construct($source=null, $options=array()) {
parent::__construct($source, $options);
}
private function getPassword() {
return $this->_source['passwd'];
}
function buildFields() {
$password = $this->getPassword();
return array(
'username' => new TextboxField(array(
'required' => true,
'label' => __('Username'),
'configuration' => array(
'length' => 0,
'autofocus' => true,
),
)),
'passwd' => new PasswordField(array(
'label' => __('Password'),
'required' => true,
'required' => !$password,
'validator' => 'noop',
'hint' => $passwdhint,
'hint' => $password
? __('Enter a new password to change current one')
: '',
'configuration' => array(
'length' => 0,
'classes' => 'span12',
'placeholder' => $passwdhint ?
str_repeat('••••••••••••', 2) : __('Password'),
'placeholder' => $password
? str_repeat('•', strlen($password)*2)
: __('Password'),
),
)),
);
}
function isValid($include=false) {
$self = $this;
return parent::isValid(function ($f) use($self) {
return !(($f instanceof PasswordField)
&& isset($self->_source['passwd'])
&& $f->resetErrors());
});
}
}
?>

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,10 +175,14 @@ 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')) {
$image = imagecreatefromstring($this->getData());
if ($scale && extension_loaded('gd')
&& ($image = imagecreatefromstring($this->getData()))) {
$width = imagesx($image);
if ($scale <= $width) {
$height = imagesy($image);
@@ -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');
@@ -477,9 +507,11 @@ class AttachmentFile extends VerySimpleModel {
elseif ($bk->write($file['data']) && $bk->flush()) {
$succeeded = true; break;
}
}
catch (Exception $e) {
} catch (Throwable $t) {
// Try next backend
// Backends can throw an exception or error.
// TODO: Log any exceptions and errors for debugging
// purposes.
}
// Fallthrough to default backend if different?
}
@@ -610,9 +642,16 @@ class AttachmentFile extends VerySimpleModel {
static function lookupByHash($hash) {
if (isset(static::$keyCache[$hash]))
return static::$keyCache[$hash];
// Cache a negative lookup if no such file exists
return parent::lookup(array('key' => $hash));
try {
return parent::lookup(array('key' => $hash));
} catch (ObjectNotUnique $e) {
// TODO: Figure out why key collission might be happening AND
// make key (hash) unique field in the file table as a
// protection measure. For now we're returning null to avoid possible wrong file
// being displayed.
return null;
}
}
static function lookup($id) {
@@ -693,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 {
@@ -1020,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
*/
@@ -1039,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.
*/
@@ -1047,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;
}
@@ -1068,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

@@ -502,36 +502,34 @@ extends VerySimpleModel {
if (is_array($rule)) {
if($rule["w"] || $rule["h"]) {
// Check for REGEX compile errors
if (in_array($rule["h"], array('match','not_match'))) {
$wrapped = "/".$rule["v"]."/iu";
if (false === @preg_match($rule["v"], ' ')
&& (false !== @preg_match($wrapped, ' ')))
$rule["v"] = $wrapped;
// Check for REGEX compile errors
if (in_array($rule["h"], array('match','not_match'))) {
$wrapped = "/".$rule["v"]."/iu";
if (false === @preg_match($rule["v"], ' ')
&& (false !== @preg_match($wrapped, ' ')))
$rule["v"] = $wrapped;
}
if(!$rule["w"] || !in_array($rule["w"],$matches))
$errors["rule_$i"]=__('Invalid match selection');
elseif(!$rule["h"] || !in_array($rule["h"],$types))
$errors["rule_$i"]=__('Invalid match type selection');
elseif(!$rule["v"])
$errors["rule_$i"]=__('Value required');
elseif($rule["w"]=='email'
&& $rule["h"]=='equal'
&& !Validator::is_email($rule["v"]))
$errors["rule_$i"]=__('Valid email required for the match type');
elseif (in_array($rule["h"], array('match','not_match'))
&& (false === @preg_match($rule["v"], ' ')))
$errors["rule_$i"] = sprintf(__('Regex compile error: (#%s)'),
preg_last_error());
else //for everything-else...we assume it's valid.
$rules[]=array('what'=>$rule["w"],
'how'=>$rule["h"],'val'=>trim($rule["v"]));
} elseif($rule["v"]) {
$errors["rule_$i"]=__('Incomplete selection');
}
if(!$rule["w"] || !in_array($rule["w"],$matches))
$errors["rule_$i"]=__('Invalid match selection');
elseif(!$rule["h"] || !in_array($rule["h"],$types))
$errors["rule_$i"]=__('Invalid match type selection');
elseif(!$rule["v"])
$errors["rule_$i"]=__('Value required');
elseif($rule["w"]=='email'
&& $rule["h"]=='equal'
&& !Validator::is_email($rule["v"]))
$errors["rule_$i"]=__('Valid email required for the match type');
elseif (in_array($rule["h"], array('match','not_match'))
&& (false === @preg_match($rule["v"], ' ')))
$errors["rule_$i"] = sprintf(__('Regex compile error: (#%s)'),
preg_last_error());
else //for everything-else...we assume it's valid.
$rules[]=array('what'=>$rule["w"],
'how'=>$rule["h"],'val'=>trim($rule["v"]));
}elseif($rule["v"]) {
$errors["rule_$i"]=__('Incomplete selection');
}
}
}
@@ -566,6 +564,15 @@ extends VerySimpleModel {
return parent::save($refetch || $this->dirty);
}
static function __create($vars,&$errors) {
$filter = new static($vars);
if ($filter->save()) {
$filter->save_rules($vars, $errors);
$filter->save_actions($filter->getId(), $vars, $errors);
return $filter;
}
}
static function create($vars,&$errors) {
$filter = new static($vars);
if ($filter->save())
@@ -908,6 +915,7 @@ class TicketFilter {
);
foreach ($auto_headers as $header=>$find) {
$header = strtolower($header);
if(!isset($headers[$header])) continue;
$value = strtoupper($headers[$header]);
@@ -931,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(
@@ -947,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

@@ -25,6 +25,7 @@ class Form {
var $options = array();
var $fields = array();
var $title = '';
var $notice = '';
var $instructions = '';
var $validators = array();
@@ -39,6 +40,8 @@ class Form {
$this->title = $options['title'];
if (isset($options['instructions']))
$this->instructions = $options['instructions'];
if (isset($options['notice']))
$this->notice = $options['notice'];
if (isset($options['id']))
$this->id = $options['id'];
@@ -53,6 +56,10 @@ class Form {
$this->id = $id;
}
function setNotice($notice) {
$this->notice = $notice;
}
function data($source) {
foreach ($this->fields as $name=>$f)
if (isset($source[$name]))
@@ -112,6 +119,7 @@ class Form {
}
function getTitle() { return $this->title; }
function getNotice() { return $this->notice; }
function getInstructions() { return Format::htmldecode($this->instructions); }
function getSource() { return $this->_source; }
function setSource($source) { $this->_source = $source; }
@@ -163,11 +171,12 @@ class Form {
* Transform form data to database ready clean data.
*
*/
function to_db($validate=true) {
if (!$this->isValid())
function to_db($clean=null, $validate=true) {
if (!$clean
&& !$this->isValid()
&& !($clean=$this->getClean($validate)))
return false;
$data = [];
$clean = $this->getClean($validate);
foreach ($clean as $name => $val) {
if (!($f = $this->getField($name)))
continue;
@@ -225,6 +234,9 @@ class Form {
$this->title = $options['title'];
if (isset($options['instructions']))
$this->instructions = $options['instructions'];
if (isset($options['notice']))
$this->notice = $options['notice'];
$form = $this;
$template = $options['template'] ?: 'dynamic-form.tmpl.php';
if (isset($options['staff']) && $options['staff'])
@@ -457,9 +469,14 @@ implements FormRenderer {
?>
<table class="<?php echo 'grid form' ?>">
<caption><?php echo Format::htmlchars($this->title ?: $form->getTitle()); ?>
<div><small><?php echo Format::viewableImages($form->getInstructions()); ?></small></div>
</caption>
<tbody><tr><?php for ($i=0; $i<12; $i++) echo '<td style="width:8.3333%"/>'; ?></tr></tbody>
<div><small><?php echo Format::viewableImages($form->getInstructions()); ?></small></div>
<?php
if ($form->getNotice())
echo sprintf('<div><small><p id="msg_warning">%s</p></small></div>',
Format::htmlchars($form->getNotice()));
?>
</caption>
<tbody><tr><?php for ($i=0; $i<12; $i++) echo '<td style="width:8.3333%"/>'; ?></tr></tbody>
<?php
$row_size = 12;
$cols = $row = 0;
@@ -1524,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
@@ -1772,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))
@@ -1846,7 +1867,10 @@ class ChoiceField extends FormField {
'id'=>1, 'label'=>__('Choices'), 'required'=>false, 'default'=>'',
'hint'=>__('List choices, one per line. To protect against spelling changes, specify key:value names to preserve entries if the list item names change.</br><b>Note:</b> If you have more than two choices, use a List instead.'),
'validator'=>'choices',
'configuration'=>array('html'=>false)
'configuration'=>array(
'html' => false,
'disabled' => false,
)
)),
'default' => new TextboxField(array(
'id'=>3, 'label'=>__('Default'), 'required'=>false, 'default'=>'',
@@ -2065,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));
@@ -2545,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),
@@ -2741,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 {
@@ -3304,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) {
@@ -3848,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.'),
@@ -3872,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']))
@@ -3902,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'));
@@ -3942,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;
@@ -4005,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();
@@ -4065,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) {
@@ -4630,6 +4684,8 @@ class ChoicesWidget extends Widget {
echo ' data-'.$D.'="'.Format::htmlchars($V).'"';
?>
data-placeholder="<?php echo Format::htmlchars($prompt); ?>"
<?php if ($config['disabled'])
echo ' disabled="disabled"'; ?>
<?php if ($config['multiselect'])
echo ' multiple="multiple"'; ?>>
<?php if ($showdefault || (!$have_def && !$config['multiselect'])) { ?>
@@ -5192,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);
@@ -5255,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')
@@ -5277,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()); ?>,
@@ -5379,6 +5438,14 @@ class FreeTextField extends FormField {
return true;
}
function isEditableToStaff() {
return $this->isVisibleToStaff();
}
function isEditableToUsers() {
return $this->isVisibleToUsers();
}
/* utils */
function to_config($config) {
@@ -6077,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,
@@ -6119,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

@@ -49,14 +49,14 @@ class Internationalization {
function loadDefaultData() {
# notrans -- do not translate the contents of this array
$models = array(
'department.yaml' => 'Dept::__create',
'sla.yaml' => 'SLA::__create',
'department.yaml' => 'Dept::__create',
'form.yaml' => 'DynamicForm::create',
'list.yaml' => 'DynamicList::create',
// Note that department, sla, and forms are required for
// help_topic
'help_topic.yaml' => 'Topic::__create',
'filter.yaml' => 'Filter::create',
'filter.yaml' => 'Filter::__create',
'team.yaml' => 'Team::__create',
// Organization
'organization.yaml' => 'Organization::__create',
@@ -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

@@ -31,8 +31,6 @@ class CsvImporter {
function __construct($stream) {
// File upload
if (is_array($stream) && !$stream['error']) {
// Properly detect Macintosh style line endings
ini_set('auto_detect_line_endings', true);
$this->stream = fopen($stream['tmp_name'], 'r');
// Skip Byte Order Mark (BOM) if present
if (!self::isBOM(fgets($this->stream, 4)))
@@ -65,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();
@@ -172,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;
@@ -191,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

@@ -596,8 +596,6 @@ class DynamicList extends VerySimpleModel implements CustomList {
function importFromPost($stuff, $extra=array()) {
if (is_array($stuff) && !$stuff['error']) {
// Properly detect Macintosh style line endings
ini_set('auto_detect_line_endings', true);
$stream = fopen($stuff['tmp_name'], 'r');
}
elseif ($stuff) {

View File

@@ -21,25 +21,73 @@ 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 {
private $mimeMessage = null;
// Charset
private $charset = 'utf-8';
// Message Id (mid)
private $mid;
// MimeMessage Parts
private $mimeParts = null;
// MimeMessage Content
private $mimeContent = null;
// Default Charset
protected $charset = 'utf-8';
// Default Encoding (upstream is ASCII)
protected $encoding = 'utf-8';
// Internal flags used to set Content-Type
private $hasHtml = false;
private $hasAttachments = false;
private $hosInlineImages = false;
private $hasInlineImages = false;
public function getMimeMessage() {
if (!isset($this->mimeMessage))
$this->mimeMessage = new MimeMessage();
return $this->mimeMessage;
public function hasAttachments() {
return $this->hasAttachments;
}
public function addHeader($key, $value) {
return $this->getHeaders()->addHeaderLine($key, $value);
public function hasInlineImages() {
return $this->hasInlineImages;
}
public function hasHtml() {
return $this->hasHtml;
}
// Files either attached or inline
public function hasFiles() {
return ($this->hasAttachments() || $this->hasInlineImages());
}
public function getId() {
if (!isset($this->mid)
&& ($header=$this->getHeader('message-id')))
$this->mid = $header->getId();
return $this->mid;
}
public function getMimeMessageParts() {
if (!isset($this->mimeParts))
$this->mimeParts = new MimeMessage();
return $this->mimeParts;
}
public function getMimeMessageContent() {
if (!isset($this->mimeContent))
$this->mimeContent = new ContentMimeMessage();
return $this->mimeContent;
}
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) {
@@ -48,16 +96,19 @@ namespace osTicket\Mail {
}
private function addMimePart(MimePart $part) {
$this->getMimeMessage()->addPart($part);
$this->getMimeMessageParts()->addPart($part);
}
private function addMimeContent(MimePart $part) {
$this->getMimeMessageContent()->addPart($part);
}
public function setTextBody($text, $encoding=false) {
$part = new MimePart($text);
$part->type = Mime::TYPE_TEXT;
$part->charset = $this->charset;
$part->encoding = $encoding ?: Mime::ENCODING_BASE64;
$this->addMimePart($part);
//$this->setContentType($alt ? 'multipart/alternative' : 'text/plain');
$part->encoding = $encoding;
$this->addMimeContent($part);
}
public function setHtmlBody($html, $encoding=false) {
@@ -65,41 +116,138 @@ namespace osTicket\Mail {
$part->type = Mime::TYPE_HTML;
$part->charset = $this->charset;
$part->encoding = $encoding ?: Mime::ENCODING_BASE64;
$this->addMimePart($part);
$this->addMimeContent($part);
$this->hasHtml = true;
}
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 = $file->getMimeType();
$f->filename = $file->getName();
$f->disposition = Mime::DISPOSITION_INLINE;
$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 setContentType($type=null) {
// We can only set content type for multipart message
if ($this->body->isMultiPart()) {
// Set Content-Type
$contentType = $type ?: 'text/plain';
if ($this->hasHtml)
$contentType = 'multipart/alternative';
if ($this->hasAttachments)
$contentType = 'multipart/related';
// 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($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) {
// We can only set content type for multipart message
if (isset($this->body)
&& $this->body->isMultiPart()
&& $contentType) {
if (($header=$this->getHeaders()->get('Content-Type')))
$header->setType($contentType); #nolint
else
@@ -108,9 +256,67 @@ namespace osTicket\Mail {
}
public function setBody($body=null) {
$body = $body ?: $this->getMimeMessage();
// We're ignoring $body param on purpose - only added for
// upstream compatibility - local interfaces should use
// prepare() to set the body
$body = $this->getMimeMessageContent();
$contentType = $this->hasHtml()
? Mime::MULTIPART_ALTERNATIVE
: Mime::TYPE_TEXT;
// if we have files (inline images or attachments)
if ($this->hasFiles()) {
// Content MimePart
$content = $body->getContentMimePart();
// Get attachments parts (inline and files)
$parts = $this->getMimeMessageParts()->getParts();
// prepend content part to files parts
array_unshift($parts, $content);
// Create a new Mime Message and set parts
$body = new MimeMessage();
$body->setParts($parts); #nolint
// We we only have inline images then content type is related
// otherwise it's mixed.
$contentType = $this->hasAttachments()
? Mime::MULTIPART_MIXED
: Mime::MULTIPART_RELATED;
}
// Set body beaches
parent::setBody($body);
$this->setContentType();
// Set the content type
$this->setContentType($contentType);
}
public function prepare() {
if (!isset($this->body))
$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
// alternative content when email is multipart
class ContentMimeMessage extends MimeMessage {
public function getContent() {
// unpack content parts to a content mime part
return $this->generateMessage(); #nolint
}
public function getContentMimePart($type=null) {
$part = new MimePart($this->getContent()); #nolint
$part->type = $type ?: Mime::MULTIPART_ALTERNATIVE;
// Set the alternate content boundary
$part->setBoundary($this->getMime()->boundary()); #nolint
// Clear the encoding
$part->encoding = "";
return $part;
}
}
@@ -118,21 +324,23 @@ namespace osTicket\Mail {
use Laminas\Mail\Protocol\Imap as ImapProtocol;
use Laminas\Mail\Protocol\Pop3 as Pop3Protocol;
trait MailBoxProtocolTrait {
final public function init(AccountOptions $accountOptions) {
final public function init(AccountSetting $setting) {
// Attempt to connect to the mail server
$connect = $accountOptions->getConnectioOptions();
$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 = $accountOptions->getAuth();
$auth = $setting->getAuthCredentials();
switch (true) {
case $auth instanceof BasicAuthCredentials:
if (!$this->basicAuth($auth->getUsername(), $auth->getPassword()))
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:
@@ -148,14 +356,14 @@ namespace osTicket\Mail {
return $this->login($username, $password);
}
abstract public function __construct($accountOptions);
abstract private function oauth2Auth(AccessToken $token);
abstract public function __construct($accountSetting);
abstract protected function oauth2Auth($authen);
}
class ImapMailboxProtocol extends ImapProtocol {
use MailBoxProtocolTrait;
public function __construct($accountOptions) {
$this->init($accountOptions);
public function __construct($accountSetting) {
$this->init($accountSetting);
}
/*
@@ -167,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 = '';
@@ -194,8 +401,8 @@ namespace osTicket\Mail {
class Pop3MailboxProtocol extends Pop3Protocol {
use MailBoxProtocolTrait;
public function __construct($accountOptions) {
$this->init($accountOptions);
public function __construct($accountSetting) {
$this->init($accountSetting);
}
/*
@@ -206,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;
}
@@ -240,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
@@ -248,9 +463,15 @@ namespace osTicket\Mail {
use RecursiveIteratorIterator;
trait MailBoxStorageTrait {
private $folder;
private $hostInfo;
private function init(\MailBoxAccount $mailbox) {
$this->folder = $mailbox->getFolder();
private function init(AccountSetting $setting) {
$this->folder = $setting->getAccount()->getFolder();
$this->hostInfo = $setting->getHostInfo();
}
public function getHostInfo() {
return $this->hostInfo;
}
private function getFolder() {
@@ -271,8 +492,7 @@ namespace osTicket\Mail {
public function hasFolder($folder, $rootFolder = null) {
$folders = $this->getFolders($rootFolder);
if (is_array($folders)
&& in_array(strtolower($folder),
array_map('strtolower', $folders)))
&& in_array(strtolower($folder), $folders))
return true;
// Try selecting the folder.
@@ -295,23 +515,52 @@ namespace osTicket\Mail {
foreach ($folders as $name => $folder) {
if (!$folder->isSelectable()) #nolint
continue;
$this->folders[] = $folder->getGlobalName(); #nolint
$this->folders[] = strtolower($folder->getGlobalName()); #nolint
}
}
return $this->folders;
}
/*
* getRawEmail
*
* Given message number - get full raw email (headers + content)
*
*/
public function getRawEmail(int $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($i, $folder) {
parent::moveMessage($i, $folder);
return true;
}
/*
* Remove a message from server.
*
* Caller should catch possible exception
*/
public function removeMessage($i) {
parent::removeMessage($i);
return true;
}
/*
* markAsSeen
*/
public function markAsSeen($i) {
// noop - storage that implement it should define it
}
public function markAsSeen($i) {
// noop - storage that implement it should define it
}
public function expunge() {
public function expunge() {
// noop - only IMAP
}
}
}
// Imap
@@ -320,15 +569,40 @@ namespace osTicket\Mail {
use MailBoxStorageTrait;
private $folders;
public function __construct($accountOptions) {
$protocol = new ImapMailBoxProtocol($accountOptions);
public function __construct($accountSetting) {
$protocol = new ImapMailBoxProtocol($accountSetting);
parent::__construct($protocol);
$this->init($accountOptions->getAccount());
$this->init($accountSetting);
}
// Mark message as seen
public function markAsSeen($i) {
$this->setFlags($i, [Storage::FLAG_SEEN]);
try {
return $this->setFlags($i, [Storage::FLAG_SEEN]);
} catch (\Throwable $t) {
return false;
}
}
/**
* 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
@@ -341,10 +615,10 @@ namespace osTicket\Mail {
class Pop3 extends Pop3Storage {
use MailBoxStorageTrait;
public function __construct($accountOptions) {
$protocol = new Pop3MailboxProtocol($accountOptions);
public function __construct($accountSetting) {
$protocol = new Pop3MailboxProtocol($accountSetting);
parent::__construct($protocol);
$this->init($accountOptions->getAccount());
$this->init($accountSetting);
}
}
@@ -374,11 +648,9 @@ namespace osTicket\Mail {
}
public function sendMessage(Message $message) {
// Make sure the body is set
if (!isset($message->body))
$message->setBody();
try {
// Make sure the body is set
$message->prepare();
parent::send($message);
} catch (\Throwable $ex) {
$this->connected = false;
@@ -391,25 +663,30 @@ namespace osTicket\Mail {
// SmtpOptions
use Laminas\Mail\Transport\SmtpOptions as SmtpSettings;
class SmtpOptions extends SmtpSettings {
public function __construct(AccountOptions $options) {
parent::__construct($this->buildOptions($options));
public function __construct(AccountSetting $setting) {
parent::__construct($this->buildOptions($setting));
}
private function buildOptions(AccountOptions $options) {
// Build out SmtpOptions options based on SmtpAccount Settings
$config = [];
$connect = $options->getConnectioOptions();
$auth = $options->getAuth();
// Build out SmtpOptions options based on SmtpAccount Settings
private function buildOptions(AccountSetting $setting) {
// 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'],
@@ -419,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'],
];
@@ -429,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
];
@@ -445,16 +724,15 @@ namespace osTicket\Mail {
}
public function sendMessage(Message $message) {
// Make sure the body is set
if (!isset($message->body))
$message->setBody();
try {
// Make sure the body is set
$message->prepare();
parent::send($message);
return true;
} catch (\Throwable $ex) {
throw $ex;
}
return true;
}
}
@@ -567,8 +845,18 @@ namespace osTicket\Mail {
return $this->token;
}
public function getAccessToken() {
return $this->getToken();
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
return (!$signature
|| !strcmp($signature, $token->getConfigSignature()))
? $token : null;
}
public function toArray() {
@@ -576,60 +864,88 @@ namespace osTicket\Mail {
}
}
// osTicket/Mail/AccountOptions
class AccountOptions {
// osTicket/Mail/AccountSetting
class AccountSetting {
private $account;
private $connectOptions = [];
private $creds;
private $connection = [];
private $errors = [];
public function __construct(\EmailAccount $account) {
// Set the account
$this->account = $account;
$this->account = &$account;
// Parse Connection Options
// We allow scheme to hint for encryption for people using ssl or tls
// on nonstandard ports.
$host = $account->getHost();
$ssl = $account->getEncryption();
$port = (int) $account->getPort();
$ssl = null;
$matches = [];
if (preg_match('~^(ssl|tls)://(.*+)$~iu', $host, $matches))
list(, $host, $ssl) = $matches;
// Set ssl or tls on based on standard ports
$port = $account->getPort();
if (!$ssl && $port) {
if (preg_match('~^(ssl|tls|plain)://(.*+)$~iu',
strtolower($host), $matches)) {
list(, $ssl, $host) = $matches;
// 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';
}
$this->connectOptions = [
// Set the connection settings
$this->connection = [
'host' => $host,
'port' => (int) $port,
'port' => $port,
'ssl' => $ssl,
'name' => null
'protocol' => strtoupper($account->getProtocol()),
'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->connectOptions['name'];
return $this->connection['name'];
}
public function getHost() {
return $this->connectOptions['host'];
return $this->connection['host'];
}
public function getPort() {
return $this->connectOptions['port'];
return $this->connection['port'];
}
public function getSsl() {
return $this->connectOptions['ssl'];
return $this->connection['ssl'];
}
public function getProtocol() {
return $this->connection['protocol'];
}
public function setCredentials(AuthCredentials $creds) {
$this->creds = $creds;
}
public function getCredentials() {
return $this->account->getCredentials();
if (!isset($this->creds))
$this->creds = $this->account->getCredentials();
return $this->creds;
}
public function getAuth() {
public function getAuthCredentials() {
return $this->getCredentials();
}
@@ -637,8 +953,73 @@ namespace osTicket\Mail {
return $this->account;
}
public function getConnectioOptions() {
return $this->connectOptions;
public function getConnectionConfig() {
return $this->connection;
}
public function getHostInfo() {
return $this->describe();
}
public function asArray() {
return $this->getConnectionConfig();
}
public function describe() {
return sprintf('%s://%s:%s/%s',
$this->getSsl() ?: 'plain',
$this->getHost(),
$this->getPort(),
$this->getProtocol());
}
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'));
}
}
return !count($this->errors);
}
public function isValid() {
return $this->validate();
}
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');
}
}
}
@@ -684,10 +1065,34 @@ namespace osTicket\Mail\Protocol\Smtp\Auth {
if (!($xoauth2=$this->getAuthRequest()))
throw new Exception('XOAUTH2 Required');
$this->_send('AUTH XOAUTH2');
$this->_expect(234);
$this->_expect(334);
$this->_send($xoauth2);
$this->_expect(235);
$this->auth = true;
}
}
}
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

@@ -18,35 +18,36 @@
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
&& ($smtp=$cfg->getDefaultMTA())
&& $smtp->isActive()) {
$this->accounts[$smtp->getId()] = $smtp;
if ($smtp->allowSpoofing() || !$email)
$email = $smtp->getEmail();
}
if (!$email && $cfg && ($email=$cfg->getDefaultEmail())) {
if (($smtp=$email->getSmtpAccount(false)) && $smtp->isActive())
$this->accounts[$smtp->getId()] = $smtp;
if ($cfg) {
// Get Default MTA (SMTP)
if (($smtp=$cfg->getDefaultMTA()) && $smtp->isActive()) {
$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->smtpAccounts[$smtp->getId()] = $smtp;
}
}
$this->email = $email;
@@ -63,7 +64,7 @@ class Mailer {
}
function getSmtpAccounts() {
return $this->accounts;
return $this->smtpAccounts;
}
function getEmail() {
@@ -71,20 +72,36 @@ class Mailer {
}
/* FROM Address */
function setFromAddress($from) {
$this->ht['from'] = $from;
function setFromAddress($from=null, $name=null) {
if ($from instanceof \EmailAddress)
$this->from = $from;
elseif (\Validator::is_email($from)) {
$this->from = new \EmailAddress(
str_contains($from, '<') ? $from : sprintf('"%s" <%s>', $name ?: '', $from));
} elseif (is_string($from))
$this->from = new \EmailAddress($from);
elseif (($email=$this->getEmail())) {
// we're assuming from was null or unexpected monster
$address = sprintf('"%s" <%s>',
$name ?: $email->getName(),
$email->getEmail());
$this->from = new \EmailAddress($address);
}
}
function getFromAddress($options=array()) {
if (!isset($this->from))
$this->setFromAddress(null, $options['from_name'] ?: null);
if (!$this->ht['from'] && ($email=$this->getEmail())) {
if (($name = $options['from_name'] ?: $email->getName()))
$this->ht['from'] =sprintf('"%s" <%s>', $name, $email->getEmail());
else
$this->ht['from'] =sprintf('<%s>', $email->getEmail());
}
return $this->from;
}
return $this->ht['from'];
function getFromName() {
return $this->getFromAddress()->getName();
}
function getFromEmail() {
return $this->getFromAddress()->getEmail();
}
/* attachments */
@@ -92,33 +109,42 @@ class Mailer {
return $this->attachments;
}
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;
function addAttachment(\Attachment $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;
function addAttachmentFile(\AttachmentFile $file) {
$this->attachments[$file->getUId()] = $file;
}
function addFileObject(FileObject $file) {
$this->attachments[] = $file;
function addFileObject(\FileObject $file) {
$this->attachments[$file->getUId()] = $file;
}
function addAttachments($attachments) {
foreach ($attachments as $a) {
if ($a instanceof Attachment)
if ($a instanceof \Attachment)
$this->addAttachment($a);
elseif ($a instanceof AttachmentFile)
elseif ($a instanceof \AttachmentFile)
$this->addAttachmentFile($a);
elseif ($a instanceof FileObject)
elseif ($a instanceof \FileObject)
$this->addFileObject($a);
}
}
/**
* 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
*
@@ -320,18 +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 basic headers
$message->addFrom($this->getFromAddress($options));
// Set our custom Message-Id
$message->setMessageId($messageId);
// Set From Address
$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;
@@ -381,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'])
@@ -406,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);
@@ -426,40 +447,49 @@ class Mailer {
foreach ($recipients as $recipient) {
if ($recipient instanceof \ClientSession)
$recipient = $recipient->getSessionUser();
switch (true) {
case $recipient instanceof \EmailRecipient:
$addr = sprintf('"%s" <%s>',
$recipient->getName(),
$recipient->getEmail());
switch ($recipient->getType()) {
case 'to':
$message->addTo($addr);
break;
case 'cc':
$message->addCc($addr);
break;
case 'bcc':
$message->addBcc($addr);
break;
}
break;
case $recipient instanceof \TicketOwner:
case $recipient instanceof \Staff:
$message->addTo(sprintf('"%s" <%s>',
$recipient->getName(),
$recipient->getEmail()));
break;
case $recipient instanceof \Collaborator:
$message->addCc(sprintf('"%s" <%s>',
$recipient->getName(),
$recipient->getEmail()));
break;
case $recipient instanceof \EmailAddress:
$message->addTo($recipient->getAddress());
break;
default:
// Assuming email address.
$message->addTo($recipient);
try {
switch (true) {
case $recipient instanceof \EmailRecipient:
$email = (string) $recipient->getEmail()->getEmail();
$name = (string) $recipient->getName();
switch ($recipient->getType()) {
case 'to':
$message->addTo($email, $name);
break;
case 'cc':
$message->addCc($email, $name);
break;
case 'bcc':
$message->addBcc($email, $name);
break;
}
break;
case $recipient instanceof \TicketOwner:
case $recipient instanceof \Staff:
$message->addTo((string) $recipient->getEmail(),
(string) $recipient->getName());
break;
case $recipient instanceof \Collaborator:
$message->addCc((string) $recipient->getEmail(),
(string) $recipient->getName());
break;
case $recipient instanceof \EmailAddress:
$message->addTo((string) $recipient->getEmail(),
(string) $recipient->getName());
break;
default:
// Assuming email address.
if (is_string($recipient))
$message->addTo($recipient);
}
} catch(\Exception $ex) {
$this->logWarning(sprintf("%s1\$s: %2\$s\n\n%3\$s\n",
_S("Unable to add email recipient"),
($recipient instanceof EmailContact)
? $recipient->getEmailAddress()
: (string) $recipient,
$ex->getMessage()
));
}
}
@@ -498,7 +528,7 @@ class Mailer {
if ($isHtml && $cfg && $cfg->isRichTextEnabled()) {
// Pick a domain compatible with pear Mail_Mime
$matches = array();
if (preg_match('#(@[0-9a-zA-Z\-\.]+)#', $this->getFromAddress(), $matches)) {
if (preg_match('#(@[0-9a-zA-Z\-\.]+)#', (string) $this->getFromAddress(), $matches)) {
$domain = $matches[1];
} else {
$domain = '@localhost';
@@ -508,24 +538,20 @@ 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];
$message->addInlineImage($match[1].$domain, $file);
// Don't re-attach the image below
unset($self->attachments[$file->getId()]);
return $match[0].$domain;
try {
$message->addInlineImage($match[1].$domain, $file);
// Don't re-attach the image below
unset($self->attachments[$file->getUId()]);
return $match[0].$domain;
} catch(\Exception $ex) {
$self->logWarning(sprintf("%1\$s:%2\$s\n\n%3\$s\n",
_S("Unable to retrieve email inline image"),
$match[1].$domain,
$ex->getMessage()));
}
}, $body);
// Add an HTML body
$message->setHtmlBody($body);
@@ -533,75 +559,117 @@ 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;
$message->addAttachment($file, $filename);
}
try {
$message->addAttachment($file, $filename);
} catch(\Exception $ex) {
$this->logWarning(sprintf("%1\$s:%2\$s\n\n%3\$s\n",
_S("Unable to retrieve email attachment"),
$filename,
$ex->getMessage()));
}
}
}
// set Body & Content Type
$message->setBody($message->getMimeMessage());
// Try possible SMTP Accounts - connections are cached per request
// at the account level.
foreach ($this->getSmtpAccounts() ?: [] as $account) {
foreach ($this->getSmtpAccounts() ?: [] as $smtpAccount) {
try {
if (($smtp=$account->getSmtpConnection())
// 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) $smtpAccount->getEmail()->getEmail(),
// Try to keep the name if available
$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) {
$alert = _S("Unable to email via SMTP")
.sprintf(": %1\$s (%2\$s:%3\$d)\n\n%4\$s\n",
$account->getEmail()->getEmail(),
$account->getHost(),
$account->getPort(),
$ex->getMessage());
$this->logError($alert);
continue;
// Log the SMTP error
$this->logError(sprintf("%1\$s: %2\$s (%3\$s)\n\n%4\$s\n",
_S("Unable to email via SMTP"),
$smtpAccount->getEmail()->getEmail(),
$smtpAccount->getHostInfo(),
$ex->getMessage()
));
}
// 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 ($this->getEmail())
$args = ['-f '.$this->getEmail()->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) {
$alert = _S("Unable to email via php mail function")
.sprintf("\n\n%1\$s\n", $ex->getMessage());
$this->logError($alert);
$this->logError(sprintf("%1\$s\n\n%2\$s\n",
_S("Unable to email via Sendmail"),
$ex->getMessage()
));
}
return false;
}
function log($msg, $type='warning') {
global $ost;
if (!$ost || !$msg)
return;
// No email alerts on email errors
switch ($type) {
case 'error':
return $ost->logError(_S('Mailer Error'), $msg, false);
break;
case 'warning':
default:
return $ost->logWarning(_S('Mailer Warning'), $msg, false);
}
return false;
}
function logError($error) {
global $ost;
//NOTE: Admin alert override - don't email when having email trouble!
$ost->logError(_S('Mailer Error'), $error, false);
return $this->log($error, 'error');
}
/******* Static functions ************/
function logWarning($warning) {
return $this->log($warning, 'warning');
}
//Emails using native php mail function - if DB connection doesn't exist.
//Don't use this function if you can help it.
static function sendmail($to, $subject, $message, $from, $options=null) {
static function sendmail($to, $subject, $message, $from=null, $options=null) {
$mailer = new Mailer(null, array('notice'=>true, 'nobounce'=>true));
$mailer->setFromAddress($from);
$mailer->setFromAddress($from, $options['from_name'] ?: null);
return $mailer->send($to, $subject, $message, $options);
}
}

View File

@@ -17,13 +17,14 @@
namespace osTicket\Mail;
class Fetcher {
var $account;
var $mbox;
private $account;
private $mbox;
private $api;
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);
}
@@ -31,6 +32,10 @@ class Fetcher {
return $this->account->getEmailId();
}
function getEmail() {
return $this->account->getEmail();
}
function getEmailAddress() {
return $this->account->getEmail()->getAddress();
}
@@ -39,8 +44,8 @@ class Fetcher {
return $this->account->getMaxFetch();
}
function getFolder() {
return $this->account->getFolder();
function getFetchFolder() {
return $this->account->getFetchFolder();
}
function getArchiveFolder() {
@@ -51,67 +56,148 @@ 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('cli');
return $this->api;
}
function noop() {
return ($this->mbox && $this->mbox->noop());
}
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), $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 Fetch Folder or Deleted based
// on the MailBox settings.
return true;
} catch (\EmailParseError $ex) {
// 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)));
}
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.
// Fetch headers
$header = $this->mbox->getRawHeader($i);
// Fetch content
$content = $this->mbox->getRawContent($i);
// Create raw stream
$stream = $header . $content;
// Create new controller and parser
$api = new \TicketApiController();
$parser = new \EmailDataParser();
// Parse the stream
$data = $parser->parse($stream);
$data['source'] = 'Email';
// Okay, let's create the ticket now
if ($api->processEmail($data)) {
// 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->getHost(), $this->getUsername());
$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);
}
}
/*
@@ -142,8 +228,10 @@ class Fetcher {
'last_activity__isnull' => true,
new \Q(['last_activity__lte' => $now->minus($interval)])
]);
$mailboxes = \MailBoxAccount::objects()
->filter(['active' => 1, $errors_Q, $fetch_Q]);
->filter(['active' => 1, $errors_Q, $fetch_Q])
->order_by('last_activity');
//Get max execution time so we can figure out how long we can fetch
// take fetching emails.
@@ -153,20 +241,26 @@ 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 (Exception $ex) {
} 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').": ".sprintf('%s:%d',
$mailbox->getHost(), $mailbox->getPort()).
"\n"._S('Error').": ".$ex->getMessage().
"\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).
"\n\n ".sprintf(_S('This could be connection issues related to the mail server. Next delayed login attempt in aprox. %d minutes'), $TIMEOUT);

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,154 +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'] = 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() {
@@ -766,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'));
}
@@ -1667,7 +1667,17 @@ EOF;
return $classname;
}
// Fix PHP 8.1.x Deprecation Warnings
// Serializable interface will be removed in PHP 9.x
function serialize() {
return serialize($this->__serialize());
}
function unserialize($data) {
$this->__unserialize(unserialize($data));
}
function __serialize() {
$info = get_object_vars($this);
unset($info['query']);
unset($info['limit']);
@@ -1675,11 +1685,10 @@ EOF;
unset($info['_iterator']);
unset($info['count']);
unset($info['total']);
return serialize($info);
return $info;
}
function unserialize($data) {
$data = unserialize($data);
function __unserialize($data) {
foreach ($data as $name => $val) {
$this->{$name} = $val;
}
@@ -1726,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);
}
@@ -1990,7 +1999,7 @@ implements IteratorAggregate {
return $model;
}
function getIterator() {
function getIterator(): Traversable {
$func = ($this->map) ? 'getRow' : 'getArray';
$func = array($this->resource, $func);
$cache = true;
@@ -2019,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();
@@ -2064,7 +2073,7 @@ implements IteratorAggregate {
$this->queryset = $queryset;
}
function getIterator() {
function getIterator(): Traversable {
$this->resource = $this->queryset->getQuery();
return new CallbackSimpleIterator(function() {
global $StopIteration;
@@ -2087,7 +2096,7 @@ implements IteratorAggregate {
$this->queryset = $queryset;
}
function getIterator() {
function getIterator(): Traversable {
$this->resource = $this->queryset->getQuery();
return new CallbackSimpleIterator(function() {
global $StopIteration;
@@ -2162,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__; }
);
@@ -2235,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();
@@ -2628,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;
@@ -2862,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);
}
@@ -2951,7 +2966,12 @@ class MySqlCompiler extends SqlCompiler {
$q->annotations = false;
$exec = $q->getQuery(array('nosort' => true));
$exec->sql = 'SELECT COUNT(*) FROM ('.$exec->sql.') __';
$row = $exec->getRow();
try {
$row = $exec->getRow();
} catch (mysqli_sql_exception $e) {
throw new InconsistentModelException(
'Unable to prepare query: '.db_error().' '.$exec->sql);
}
return is_array($row) ? (int) $row[0] : null;
}
@@ -3621,12 +3641,22 @@ class Q implements Serializable {
return $result;
}
// Fix PHP 8.1.x Deprecation Warnings
// Serializable interface will be removed in PHP 9.x
function serialize() {
return serialize(array($this->negated, $this->ored, $this->constraints));
return serialize($this->__serialize());
}
function unserialize($data) {
list($this->negated, $this->ored, $this->constraints) = unserialize($data);
$this->__unserialize(unserialize($data));
}
function __serialize() {
return array($this->negated, $this->ored, $this->constraints);
}
function __unserialize($data) {
list($this->negated, $this->ored, $this->constraints) = $data;
}
}
?>

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

@@ -234,10 +234,7 @@ class Page extends VerySimpleModel {
try {
return self::objects()->filter(array('type'=>$type))->one();
}
catch (DoesNotExist $ex) {
return null;
}
catch (InconsistentModelException $ex) {
catch (DoesNotExist | InconsistentModelException $ex) {
return null;
}
}

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

@@ -22,7 +22,7 @@ abstract class PluginConfig extends Config {
foreach ($this->getOptions() as $name => $field) {
if ($this->exists($name)) {
$val = $this->get($name);
$this->_config[$name] = $field->to_php($val) ?: $val;
$this->_config[$name] = $field->to_php($val);
} elseif (($default = $field->get('default')))
$this->_config[$name] = $default;
}
@@ -50,6 +50,17 @@ abstract class PluginConfig extends Config {
return array();
}
// getFields is same as getOptions but can be used downstream to filter
// or modify fields based on settings
function getFields() {
return $this->getOptions();
}
// Returns form options like title and notices as accepted by SimpleForm
function getFormOptions() {
return array();
}
function getInfo() {
return array_merge(parent::getInfo(), $this->_config);
}
@@ -58,7 +69,7 @@ abstract class PluginConfig extends Config {
return $this->instance;
}
function setInstance(PluginInstance $i=null) {
function setInstance(?PluginInstance $i=null) {
$this->instance = $i;
}
@@ -72,7 +83,8 @@ abstract class PluginConfig extends Config {
*/
function getForm($vars=[]) {
if (!isset($this->form)) {
$this->form = new SimpleForm($this->getOptions());
$this->form = new SimpleForm($this->getFields(),
null, $this->getFormOptions());
// set data if any
if ($_SERVER['REQUEST_METHOD'] != 'POST') {
// defaults + current info
@@ -104,17 +116,22 @@ abstract class PluginConfig extends Config {
* returns (true | false)
*
*/
function store(SimpleForm $form = null, &$errors=array()) {
function store(?SimpleForm $form = null, &$errors=array()) {
if ($this->hasCustomConfig())
return $this->saveConfig($form, $errors);
$form = $form ?: $this->getForm();
if (($data=$form->to_db())
&& $this->pre_save($data, $errors)
&& count($errors) === 0)
return $this->updateAll($data);
try {
if ($this->hasCustomConfig())
return $this->saveConfig($form, $errors);
$form = $form ?: $this->getForm();
if (($clean=$form->getClean())
&& $this->pre_save($clean, $errors)
&& count($errors) === 0
&& ($data=$form->to_db($clean)))
return $this->updateAll($data);
} catch (Throwable $t) {
if (!isset($errors['err']))
$errors['err'] = $t->getMessage();
}
return false;
}
@@ -351,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;
@@ -366,6 +385,8 @@ class PluginManager {
];
$p = Plugin::create($vars);
if ($p->save(true)) {
// TODO: Trigger enable() for the plugin (for now)
$p->getImpl()->enable();
static::clearCache();
return $p;
}
@@ -562,10 +583,12 @@ class Plugin extends VerySimpleModel {
function __onload() {
$this->info = PluginManager::getInfoForPath(INCLUDE_DIR.$this->ht['install_path'],
$this->isPhar());
// Auto upgrade / downgrade plugins on-load - if we have version
// mismatch. See upgrade routine for more information
if ($this->info['version'] && $this->getVersion() != $this->info['verion'])
$this->upgrade();
}
/*
* useModalConfig
*
@@ -578,6 +601,72 @@ class Plugin extends VerySimpleModel {
}
/*
* canAddInstance
*
* Multi-instance plugins can add unlimited instances, otherwise only
* one plugin instance is allowed.
*
*/
function canAddInstance() {
// No instances yet
if (!$this->getNumInstances())
return true;
// We have at least one instance already.
if (!$this->isMultiInstance())
return false;
// Some Plugins DO Not or SHOULDN'T support multiple instances due
// design issues or simply because of the fact that it doesn't make
// sense to do so.
// TODO: The Black List is to be removed down the road once plugins
// are forced to declare core osTicket version and if it supports
// multiple instances.
$blackList =[
// 2FA Plugin up to v0.3 cannot handle multiple instances
'Auth2FAPlugin' => 0.3,
// It doesn't make sense for Audit Plugin to have
// multiple instances
'AuditPlugin' => '*',
// File storage plugins don't currently support multiple instances
'S3StoragePlugin' => '*',
'FsStoragePlugin' => '*',
];
foreach ($blackList as $c => $v) {
if (is_a($this, $c) && ($v == '*' || $this->getVersion() <= $v))
return false;
}
// Yes, let's make instances - Genesis 9:7
return true;
}
/*
* Get Namespace of the instance otherwise return plugin's namespace
*
*/
function getNamespace() {
if (($c=$this->getConfig()) && ($i=$c->getInstance()))
return $i->getNamespace();
return sprintf('plugin.%d', $this->getId());
}
/*
*
* isMultiInstance
*
* Indicates if the plugin supports multiple instances
* Default is true unless overwritten downstream.
*
*/
function isMultiInstance() {
return true;
}
/*
* getNewInstanceOptions
*
@@ -691,14 +780,14 @@ 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
// plugin instantace boostrapping
if ($instance)
// Config for an instance.
$this->config = $instance->getConfig($class, $defaults);
$this->config = $instance->getConfig($defaults);
else
// New instance config
$this->config = new $class(null, $defaults);
@@ -750,6 +839,35 @@ class Plugin extends VerySimpleModel {
parent::delete();
}
/**
* upgrade
*
* Plugins can implement upgrade / downgrade process downsteam as needed.
*
*/
function upgrade(&$errors=[]) {
if ($this->pre_upgrade($errors) === false)
return false;
// For now we're just updating the version if we have a mismatch
// The true version is what is packaged with the plugin
if ($this->getVersion() != $this->info['version'])
$this->version = $this->info['version'];
$this->save();
return true;
}
/**
* pre_upgrade
*
* Hook function to veto the upgrade request. Return boolean
* FALSE if the upgrade operation should be aborted.
*/
function pre_upgrade(&$errors) {
return true;
}
/**
* uninstall
*
@@ -777,6 +895,10 @@ class Plugin extends VerySimpleModel {
return true;
}
function enable() {
return true;
}
/**
* Function: __
*
@@ -894,6 +1016,8 @@ class PluginInstance extends VerySimpleModel {
),
);
// Config class that plugin can set.
private $config_class = null;
// Plugin Config for the instance
var $_config;
var $_form;
@@ -948,13 +1072,22 @@ class PluginInstance extends VerySimpleModel {
$this->setFlag(self::FLAG_ENABLED, $status);
}
function getConfig($class=null, $defaults=[]) {
$class = $class ?: $this->getPlugin()->getConfigClass();
function setConfigClass($class) {
$this->config_class = $class;
// Clear current config so it can be reloaded
$this->_config = null;
}
function getConfigClass() {
return $this->config_class ?: $this->getPlugin()->getConfigClass();
}
function getConfig($defaults=[]) {
$class = $this->getConfigClass();
if (!isset($this->_config) && $class) {
$this->_config = new $class($this->getNamespace(), $defaults);
$this->_config->setInstance($this);
}
return $this->_config;
}
@@ -966,7 +1099,12 @@ class PluginInstance extends VerySimpleModel {
}
function getSignature() {
return md5(json_encode(ksort($this->getConfiguration())));
// get Config
$config = $this->getConfiguration() ?: [];
// Key sort to normalize config - key order matters with md5
ksort($config);
// Json encode and md5
return md5(json_encode($config));
}
function getNamespace() {

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)));
@@ -1185,7 +1186,7 @@ class CustomQueue extends VerySimpleModel {
continue;
}
$f->set('heading', $heading);
$f->set('heading', isset($fields[$key]['heading']) ? $fields[$key]['heading'] : $heading);
$f->set('sort', array_search($key, $order)+1);
unset($new[$key]);
}
@@ -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

@@ -330,6 +330,15 @@ implements AuthenticatedUser, EmailContact, TemplateVariable, Searchable {
return $this->email;
}
function getEmailAddress() {
$emailaddr = (string) $this->getEmail();
if (($name=$this->getName()))
$emailaddr = sprintf('"%s" <%s>',
(string) $name,
$emailaddr);
return $emailaddr;
}
function getAvatar($size=null) {
global $cfg;
$source = $cfg->getStaffAvatarSource();
@@ -704,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;
@@ -729,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) {
@@ -1494,6 +1503,7 @@ extends AbstractForm {
'required' => true,
'configuration' => array(
'classes' => 'span12',
'length' => '128',
),
'visibility' => new VisibilityConstraint(
new Q(array('welcome_email' => false)),
@@ -1513,6 +1523,7 @@ extends AbstractForm {
'required' => true,
'configuration' => array(
'classes' => 'span12',
'length' => '128',
),
'visibility' => new VisibilityConstraint(
new Q(array('welcome_email' => false)),
@@ -1556,6 +1567,7 @@ extends AbstractForm {
'required' => true,
'configuration' => array(
'autofocus' => true,
'length' => '128',
),
'validator' => 'noop',
)),
@@ -1563,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 {
@@ -1575,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 &lt;%s&gt; 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

@@ -667,12 +667,20 @@ class Translation extends gettext_reader implements Serializable {
}
// Fix PHP 8.1.x Deprecation Warnings
// Serializable interface will be removed in PHP 9.x
function serialize() {
return serialize(array($this->charset, $this->encode, $this->cache_translations));
return serialize($this->__serialize());
}
function unserialize($what) {
list($this->charset, $this->encode, $this->cache_translations)
= unserialize($what);
$this->__unserialize(unserialize($what));
}
function __serialize() {
return array($this->charset, $this->encode, $this->cache_translations);
}
function __unserialize($what) {
list($this->charset, $this->encode, $this->cache_translations) = $what;
$this->short_circuit = ! $this->enable_cache
= 0 < $this->cache_translations ? count($this->cache_translations) : 1;
}
@@ -768,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
@@ -737,6 +740,8 @@ implements TemplateVariable {
$this->address = sprintf('"%s" <%s>',
$this->getName(),
$this->email);
else
$this->address = $this->email;
}
function __toString() {
@@ -745,7 +750,7 @@ implements TemplateVariable {
function getVar($what) {
if (!$this->_info)
if (!isset($this->_info))
return '';
switch ($what) {
@@ -760,7 +765,11 @@ implements TemplateVariable {
}
function getAddress() {
return $this->address ?: $this->email;
return $this->address ?: $this->getEmail();
}
function getEmail() {
return $this->email;
}
function getHost() {
@@ -1320,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']);
@@ -1398,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
unset($_SESSION['KAPUT']);
// 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,20 +121,60 @@ trait UserSessionTrait {
// User class
var $class = '';
function refreshSession($force=false) {
$time = $this->session->getLastUpdate($this->token);
// Deadband session token updates to once / 30-seconds
if (!$force && time() - $time < 30)
return;
$this->token = $this->getSessionToken();
osTicketSession::renewCookie($time, $this->maxidletime);
public function getMaxIdleTime() {
return $this->maxidletime ;
}
function regenerateSession($destroy=false) {
$this->session->regenerateSession($destroy);
// Set cookie for the new session id.
$this->refreshSession(true);
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();
// Update the expire time for the session cookie
osTicketSession::renewCookie(time(), $this->getMaxIdleTime());
}
}
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() {
@@ -188,6 +189,7 @@ trait UserSessionTrait {
// Assign memory to token variable
$this->token = &$_SESSION[':token'][$this->class];
// Set token
$token = $token ?: $this->token;
$this->token = $token ?: $this->getSessionToken();
}
@@ -198,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();
@@ -212,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

@@ -8,6 +8,7 @@ interface EmailContact {
function getUserId();
function getName();
function getEmail();
function getEmailAddress();
}
@@ -15,6 +16,7 @@ class EmailRecipient
implements EmailContact {
protected $contact;
protected $type;
protected $address;
function __construct(EmailContact $contact, $type='to') {
$this->contact = $contact;
@@ -37,6 +39,16 @@ implements EmailContact {
return $this->contact->getEmail();
}
function getEmailAddress() {
if (!isset($this->address)) {
$this->address = (string) $this->getEmail();
if (($name=$this->getName()))
$this->address = sprintf('"%s" <%s>',
(string) $name, $this->address);
}
return $this->address;
}
function getName() {
return $this->contact->getName();
}
@@ -44,6 +56,11 @@ implements EmailContact {
function getType() {
return $this->type;
}
function __toString() {
return (string) $this->getEmailAddress();
}
}
abstract class BaseList
@@ -81,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);
}
@@ -178,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)
@@ -187,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);
@@ -200,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)
@@ -215,12 +233,22 @@ implements ArrayAccess, Serializable {
unset($this->storage[$offset]);
}
// Serializable
// Fix PHP 8.1.x Deprecation Warnings
// Serializable interface will be removed in PHP 9.x
function serialize() {
return serialize($this->storage);
return serialize($this->__serialize());
}
function unserialize($what) {
$this->storage = unserialize($what);
$this->__unserialize(unserialize($what));
}
// Serializable
function __serialize() {
return $this->storage;
}
function __unserialize($what) {
$this->storage = $what;
}
}

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