├── .github └── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── hooks ├── AbortEmailSendingBasedOnUserIDClientGroupID.php ├── AcceptOrderOnInvociePaid.php ├── AcceptQuoteWithoutLogin.php ├── AddButtonNextToModulesFunctions.php ├── AdminStatsForWHMCSv8.php ├── AnnuncementsMetaDescription.php ├── AssignClientToGroupBasedOnPurchasedProduct_v1.php ├── AssignClientToGroupBasedOnPurchasedProduct_v2.php ├── AssignClientToGroupBasedOnRegisteredDomains.php ├── AssignClientToGroupBasedOnRegistrationDate.php ├── AutoLoginToAnyPanelFromMyServices.php ├── AutoTerminateFreeTrialsAfterXMinutes.php ├── BanOrderExpiration.php ├── BulkAutoRecalculateClientDomainsProducts.php ├── CancelOrderOnInvoiceCancelled.php ├── ChangeDefaultSortingBackendTables.php ├── ChatstackDisableLoggedInAndAdmin.php ├── ClientGroupColorInTicketView.php ├── ConditionalClientCustomFieldsBasedOnSelectedCountry.php ├── ConditionalSupportDepartments.php ├── ContactsEmailConfirmation.php ├── CouponCodeInEmailTemplate.php ├── DailyCronJonOnDemand.php ├── DisableFeedbackRequestsForUnansweredTickets.php ├── ExemptExistingClientsFromAffiliateCommissions.php ├── ForcePaymentGatewayDependingOnInvoiceBalance.php ├── GenerateUUID.php ├── HideGoogleInvisibleReCAPTCHA.php ├── IfClientsGroupThisThenThat.php ├── InactiveIsTheNewActive.php ├── KnowledgebaseAuthor.php ├── KnowledgebaseLastUpdatedDate.php ├── LoginAsClientPreserveLanguage.php ├── NewClientsAsAffiliates.php ├── NoteInTheOrderAbortAutoProvisioning.php ├── NotifyFradulentOrders.php ├── OneOffProductsDomainRequireProduct.php ├── PreventAccessToBackendDuringMaintenance.php ├── PreventChangesToClientCustomFields.php ├── PreventEmailSendingBasedOnClientGroup.php ├── PreventSearchEngineIndexing.php ├── PromotionsArrayInEmailTemplates.php ├── QuoteToInvoiceNoRedirect.php ├── RelatedServiceInInfoTicketSidebar.php ├── RemoveIPAddressFromViewTicketClientArea.php ├── RemovePortalHomeBreadcrumb.php ├── RenameAddonModuleLabel.php ├── RestrictDomainBillingCyclesBasedOnTLD.php ├── RestrictPaymentGatewaysBasedOnClientGroup.php ├── SendEmailAndAddReplyOnTicketStatusChange.php ├── StrongerPasswordGeneratorForAutoProvisioning_v1.php ├── StrongerPasswordGeneratorForAutoProvisioning_v2.php ├── StrongerPasswordGeneratorForAutoProvisioning_v3.php ├── TicketFeedbackEscalationRule.php ├── UpdateAdminLinksWhenCustomAdminPathChanges.php └── noDatesInInvoiceItemsDescription.php ├── modules └── addons │ └── PleskChecker │ ├── PleskChecker.php │ ├── core │ ├── Katamaze │ │ ├── Checker.php │ │ ├── PleskAPIClient.php │ │ └── index.php │ └── index.php │ ├── images │ ├── index.php │ └── katamaze.png │ ├── index.php │ └── templates │ ├── Admin │ ├── Main.tpl │ └── index.php │ └── index.php └── reports └── Churn_Rate.php /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '[Bug]' 5 | labels: 'bug' 6 | assignees: 'Kian987' 7 | 8 | --- 9 | 10 | ## Before reporting a bug 11 | Make sure you're running the most up-to-date version of the action hook. 12 | 13 | ## Action Hook File Name 14 | We created a lot of action hooks hence knowing the name of the hook that is causing you a problem is mandatory. Hook name corresponds to file name (eg. `AcceptOrderOnInvociePaid.php`, `DailyCronJonOnDemand.php`). 15 | 16 | ## Describe the Bug 17 | A clear and concise description of what the bug is. 18 | 19 | ## To Reproduce 20 | Steps to reproduce the behavior: 21 | 1. Go to '...' 22 | 2. Click on '....' 23 | 3. Scroll down to '....' 24 | 4. See error 25 | 26 | ## Expected behavior 27 | A clear and concise description of what you expected to happen. 28 | 29 | ## Affected Version 30 | Please complete the following information: 31 | 32 | * WHMCS [eg. 7.10.2] 33 | * PHP [eg. 7.2.32] 34 | 35 | ## Screenshots 36 | If applicable, add screenshots to help explain your problem. 37 | 38 | ## Additional context 39 | Add any other context about the problem here. 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '[Feature Request]' 5 | labels: 'feature request' 6 | assignees: 'Kian987' 7 | 8 | --- 9 | 10 | ## Before requesting a feature 11 | We're in the process to transition from completely closed to completely open source. Before taking this important step, we're using this project to show our intent by releasing hundreds of scripts for WHMCS 100% free. 12 | 13 | Recently we started re-routing some presales from [our site](https://katamaze.com/) here on Github. Basically we're willing to code solutions for free. The only requirement is that we're open for **small and medium-sized projects** that are not in contrast with [what we are selling](https://katamaze.com/whmcs). 14 | 15 | Let's start! 16 | 17 | ## Describe the solution you'd like 18 | A clear and concise description of what you want to happen in your WHMCS. 19 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at info@katamaze.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First off, thanks for taking the time to contribuite 👍 4 | 5 | The point of this this package is to help **Developers**, **Hosting Providers**, **Web Agencies** and **IT professionals** to perfect WHMCS. Over the years we kept improving our code based on customers' feedback but together we can make it even better. 6 | 7 | Feel free to propose changes to existing scripts, request new action hooks and report bugs. 8 | 9 | Please read the following FAQ to know more about coding conventions. 10 | 11 | # Frequently Asked Question 12 | 13 | ## Why do you keep using `ClientAreaPage` when a more specific hook point is available? 14 | 15 | For us backward compatibility has always been important since we have customers still running outdated versions of WHMCS (it doesn't depend on us ☹️). That said, we know we can use `ClientAreaPageHome` in place of `ClientAreaPage` to "play" with home page. The problem is that older versions of WHMCS only have `ClientAreaPage`. That's why we keep using it. 16 | 17 | ## Can I use short open tag `` 23 | 24 | ## Can I place `use` operator wherever I need? 25 | 26 | No. All `use` statements must be on top of the file right after ` 142 | 143 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hook-Factory/blob/master/hooks/ConditionalClientCustomFieldsBasedOnSelectedCountry.php) 144 | 145 | ## Prevent Admins from accessing WHMCS backend during Maintenance 146 | 147 | Define an array of Admin and/or Admin Roles that are allowed to access WHMCS backend when Maintenance Mode is enabled. All other Admins are logged out automatically. 148 | 149 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hook-Factory/blob/master/hooks/PreventAccessToBackendDuringMaintenance.php) 150 | 151 | ## If client's group this than that 152 | 153 | Over the years I coded hundreds of hooks involving WHMCS client's group. It is common that people want to tie very specific logic to groups. The hook that I am going to present you can be used as starting point to begin playing with WHMCS client groups. 154 | 155 | I got inspiration from [IFTTT](https://ifttt.com/explore/new_to_ifttt) (If This Then That). It all starts from the initial array where you define what should happen for every client group of WHMCS. My initial version lets you apply the following rules/actions to given client groups: 156 | 157 | * Enable/disable tax exempt status 158 | * Replace `Pay to` text of invoices with anything you want 159 | * Define an array of allowed payment methods. This implies that other payment methods are restriced 160 | 161 | In the example you find in the script, I prepared the following scenario: 162 | 163 | * Clients belonging to group id `1`: 164 | * Tax exempt: Enabled 165 | * Default Invoice header (Pay To) replaced with `Ferrari S.p.A. ...` 166 | * Allowed payment methods: `paypalcheckout` (default), `banktransfer`. All other payment methods get automatically removed from open invoices (not in `Paid`, `Collections`, `Refund`, `Payment Pending` status). Moreover the script also removes them from `Payment Method` dropdown accessible from `viewinvoice.php` 167 | * Clients belonging to group id `2`: 168 | * Tax exempt: Disabled 169 | * Default Invoice header (Pay To) replaced with `Juventus S.p.A. ...` 170 | * Allowed payment methods: `banktransfer` (default). Same principle previously described. Other gateways get restricted from open invoices and dropdown 171 | 172 | You can extend this hook to meet your specific goals. For example you could add conditional invoice logo rather than currency, language, email preferences etc. 173 | 174 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hook-Factory/blob/master/hooks/IfClientsGroupThisThenThat.php) 175 | 176 | ## Restrict payment gateways based on client group 177 | 178 | As the title says, define what payment gateways each client group is allowed to use to pay invoices. The hook automatically removes restricted payment gateways from `viewinvoice.php` page (Paymeth Method dropdown menu). It also replaces restricted gateways from open invoices (not in `Paid`, `Collections`, `Refund`, `Payment Pending` status) with the first gateway you defined in `allowed_payment_gateways` array. 179 | 180 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hook-Factory/blob/master/hooks/RestrictPaymentGatewaysBasedOnClientGroup.php) 181 | 182 | ## Admin Stats for WHMCS v8 183 | 184 | As you probably know WHMCS v8 no longer provides statistics on top of the page about pending orders, overdue invoices and tickets awaiting reply. This action hook adds them back to interface as you can see from the following screenshot. 185 | 186 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-admin-bar-stats-navbar.png) 187 | 188 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-admin-bar-stats-sidebar.png) 189 | 190 | The one thing you can customize is `$showZero` variable. Let's focus on the *0 Ticket(s) Awaiting Reply* badge of above images. If `$showZero` is set to `false`, the widget doesn't show this specific badge. 191 | 192 | This widget is fully responsive and appears if there's at least one pending order, overdue invoice or ticket awaiting reply. If there's nothing to show it disappears. To avoid any possibility of confusion, the hook automatically detects if you're running v8. 193 | 194 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/AdminStatsForWHMCSv8.php) 195 | 196 | ## Client Group Color in Ticket View for WHMCS v8 197 | 198 | Client group background colors no longer display on ticket view page. Go figure out why WHMCS decided to remove it from v8. This action hook puts the styling back. 199 | 200 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-client-group-color-ticket-view.png) 201 | 202 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Scripts/blob/master/hooks/ClientGroupColorInTicketView.php) 203 | 204 | ## Simulate / Run WHMCS Daily Cron Job on Demand 205 | 206 | As the name suggests, WHMCS daily cron job runs once per day. There's no easy way to make it run multiple times. This could be frustrating in case you're coding or testing new features that's where this hook comes to help. 207 | 208 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-live-demo.png) 209 | 210 | The hook adds *Run Daily Cronjob* button (the orange one) on top of your WHMCS Administration. Clicking it allows to run WHMCS daily cron job whenever you want. All it takes is a click. Please, ignore *Reinstall* and *Manage Demo* buttons. We use them for [Live Demo](https://katamaze.com/demo) to let visitors try our modules before purchase. 211 | 212 | Be advised that on WHMCS v8 and newer versions the *Run Daily Cronjob* button is placed on top of the sidebar as shown in the screenshot. 213 | 214 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-run-cron-on-demand.png) 215 | 216 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/DailyCronJonOnDemand.php) 217 | 218 | ## Accept Quote without Logging In 219 | 220 | When you send a quote, WHMCS forces customers to login in order to accept it. This hook allows them to accept without the need to login. Every time the *Quote Delivery with PDF* mail is sent, the hook overrides `{$quote_link}` with a new link that contains an hash that ensures the authenticity of the request. This way only the recipient can accept the quote. 221 | 222 | When the visitor clicks the link, the quote is automatically accepted and he/she sees the following modal on screen. 223 | 224 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/quote-accepted.png) 225 | 226 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/AcceptQuoteWithoutLogin.php) 227 | 228 | ## Bulk Auto Recalculate Client Domain & Products/Services 229 | 230 | Yes, WHMCS integrates [Bulk Pricing Updater](https://docs.whmcs.com/Bulk_Pricing_Updater_Addon) but it works for all existing customers. Sometimes you simply need to recalculate prices for domains and products/services of a specific customer. This hook allows to do that in one click. First it adds the following button in client Summary. 231 | 232 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-bulk-auto-recalculate-customer.png) 233 | 234 | Second it shows this modal on screen where you can freely choose to auto-recalculate domains or products/services. 235 | 236 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-bulk-auto-recalculate-customer-domain-product.png) 237 | 238 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/BulkAutoRecalculateClientDomainsProducts.php) 239 | 240 | ## No Dates in Invoice Items Description 241 | 242 | When it comes to service/domain renewal, WHMCS always puts dates in invoice description like so `Hosting Silver - example.com (10/05/2022 - 09/05/2023)`. With this hook you can get rid of the ` (10/05/2022 - 09/05/2023)` part (initial space included). The hook automatically detects what is the Date Format in use on your WHMCS: 243 | 244 | * `DD/MM/YYYY` 245 | * `DD.MM.YYYY` 246 | * `DD-MM-YYYY` 247 | * `MM/DD/YYYY` 248 | * `YYYY/MM/DD` 249 | * `YYYY-MM-DD` 250 | 251 | This way it always uses the right regex to match the string. The hook triggers on `InvoiceCreationPreEmail` hence it will not affect your existing invoices. Lastly it works also with multiple-lines descriptions (eg. addons, configurable options). 252 | 253 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hook-Factory/blob/master/hooks/noDatesInInvoiceItemsDescription.php) 254 | 255 | ## cPanel & Plesk login button in My Services 256 | 257 | Managing multiple hosting accounts could be frustrating for customers. The following hook makes things easier allowing them to login to any control panel directly from My Services list. Here's the preview. 258 | 259 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-login-to-plesk-cpanel-from-service-list.png) 260 | 261 | The hook works with any panel (cPanel, Plesk, DirectAdmin, Centova Cast...) provided that servers and products/services have been intgrated correctly. Before you get the code, keep in mind that this action hook requires some changes to a template file. 262 | 263 | Open `templates/{YOUR_TEMPLATE}/clientareaproducts.tpl` and add the new *Manage* column in `thead` like follows. 264 | 265 | ``` 266 | 267 | 268 | {$LANG.orderproduct} 269 | {$LANG.clientareaaddonpricing} 270 | {$LANG.clientareahostingnextduedate} 271 | {$LANG.clientareastatus} 272 | {$LANG.manage} 273 | 274 | 275 | 276 | ``` 277 | 278 | Your `thead` could be slightly different (eg. your first column could be the SSL icon check) so change things accordingly. Next move to `tbody` and add the cell right inside `{foreach}` loop. 279 | 280 | ``` 281 | 282 | {if $kt_autologin[$service.id]} 283 |
284 | 285 |
286 | {/if} 287 | 288 | ``` 289 | 290 | We suggest you to replace *Click to Login* with `$LANG` variable for multi-language support. Now we need to disable sorting for the newly added column. On top of the file you'll find the following statement. 291 | 292 | ``` 293 | {include file="$template/includes/tablelist.tpl" tableName="ServicesList" noSortColumns="4" filterColumn="3"} 294 | ``` 295 | 296 | Focus on `noSortColumns="4"`. *4* means that the 5th column will be not sortable (column count start from zero). Change it accordingly. For example if your template uses the SSL check as 1st column, use `noSortColumns="0, 5"`. 297 | 298 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/AutoLoginToAnyPanelFromMyServices.php) 299 | 300 | ## Related Service in Ticket Sidebar 301 | 302 | Customers can specify the related service/domain on ticket submission but once the ticket has been sent the information is no longer visible. This hook makes sure that related service is always included in ticket sidebar (if specified). 303 | 304 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-related-service-domain-in-ticket-sidebar.PNG) 305 | 306 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/RelatedServiceInInfoTicketSidebar.php) 307 | 308 | ## Force Payment Gateway depending on Invoice Balance 309 | 310 | It doesn't matter what payment method you use. It can be PayPal, Stripe, Skrill or Credit Card. The typical gateway charges absurdly high fees to manage your money. [Billing Extension](https://katamaze.com/whmcs/billing-extension/specifications) helps you [saving up to 18% on transaction fees](https://katamaze.com/docs/billing-extension/4/reducing-the-number-of-invoices#OnePayment) but such costs can be lowered even further. 311 | 312 | Let's face it. In an ideal world we would be receiving money just with Bank Transfer (aka Wire Transfer) since it doesn't cost you anything. The following hook can be used to force the most convenient gateway you have depending on invoice balance. For example *if invoice balance >= 1000 euro force banktransfer*. Let's do some math. 313 | 314 | * PayPal charges 3.4% + 0.35 € per transaction meaning that receiving 1000 € costs you 35.35 € 315 | * Let's suppose on a yearly basis you receive 10 payments of 1000 € 316 | * At the end of the year you gave to PayPal 353.5 € 317 | 318 | With this hook you can keep this money for you. As if it wasn't enough, the hook can be customized to force the payment gateway depending on customers' country. For example you can use the hook just for specific countries (eg. IT, FR, DE) and/or European Union. Don't worry about multiple currencies. The script automatically handles currency conversion when needed. 319 | 320 | It is worth to say that the hook allows administrators to lift restrictions on specific invoices. All you all you need is changing the payment method from `Options` tab (Invoice View). This will add the following note `Payment Method Unlocked by Administratror` that serves as a way to let customers freely choose their gateway. 321 | 322 | Of course we don't want such note to be visible in front-end hence the hook automatically removes it from the HTML version invoices. As for the PDF version, you'll need to place a small piece of code right above `if ($notes)` statement in your `invoicepdf.tpl` as follows: 323 | 324 | ``` 325 | # Notes 326 | $notes = str_replace('Payment Method Unlocked by Administratror', '', $notes); 327 | $notes = ($notes ? $notes : false); 328 | if ($notes) { 329 | $pdf->Ln(5); 330 | $pdf->SetFont($pdfFont, '', 8); 331 | $pdf->MultiCell(170, 5, Lang::trans('invoicesnotes') . ': ' . $notes); 332 | } 333 | ``` 334 | 335 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/ForcePaymentGatewayDependingOnInvoiceBalance.php) 336 | 337 | ## Auto-Terminate Free Trials After X Minutes 338 | 339 | Free trials for a limited period is a good marketing strategy to capitalize on the leads you get. The problem with trials is that the smallest unit of time for WHMCS is the day meaning that for example you can't provide a trial for VPS that last for a couple of hours. WHMCS can't "think" for a period of less than a full day. 340 | 341 | The following action hook allows to automatically terminate or suspend the given products/services after a certain number of minutes. It runs AfterCronJob hook point that normally triggers once every 5 minutes. Visit Setup > Automation Settings and make sure that cron.php runs every 5 minutes as suggested by WHMCS. The hook will do the rest. It also logs performed actions in Activity Log. Here are the variables you need to configure: 342 | 343 | * `$productIDs` array of Product ID you want to terminate or suspend 344 | * `$terminateAfter` terminate or suspend products after the given number of minutes (1440 = full day - 0 to disable) `integer` 345 | * `$performAction` can choose between `Terminate` and `Suspend` 346 | 347 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/AutoTerminateFreeTrialsAfterXMinutes.php) 348 | 349 | ## Stronger Password Generator for Auto-Provisioning 350 | 351 | We give you not one, not two but three action hooks to override default passwords generated by WHMCS for service provisioning on third-party control panels like Plesk, cPanel, DirectAdmin and custom-made server modules. 352 | 353 | * [v1](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/StrongerPasswordGeneratorForAutoProvisioning_v1.php) randomly picks 10 characters from `a-zA-Z0-9` and `!@#$%^&*()-=+?` 354 | * [v2](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/StrongerPasswordGeneratorForAutoProvisioning_v2.php) same as above but makes sure that at least one special character is included in the password 355 | * [v3](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/StrongerPasswordGeneratorForAutoProvisioning_v3.php) for extremely strong passwords. Individually define the number of digits, lowercase, uppercase and special characters to use. The resulting password will not use the same character twice 356 | 357 | ## One-off Products/Services & Domain purchase requires Product/Service 358 | 359 | If you have a bit of experience with WHMCS, you know that offering promotions just via [coupon codes](https://docs.whmcs.com/Promotions) isn't so flexible. 360 | 361 | Many prefer to have products/services created specifically for special deals. Similarly others want to restrict domain purchase to customers with at least a product/service in their accounts. The hook lets you achieve both goals. Simply configure the following variables: 362 | 363 | * `kt_onetimeProducts` array of product IDs to treat as "one-off" (customer is not allowed to order the same product multiple times) 364 | * `kt_onetimeProductGroups` same as above but for product group IDs. Producs inside such groups are treated as one-off 365 | * `kt_firstTimerTollerance` product-based restrictions are disabled for new customers placing their first order with you 366 | * `kt_notRepeatable` if a customer already has a one-off product, he can't purchase further one-offs (`kt_firstTimerTollerance` is ignored) 367 | * `kt_domainRequiresProduct` domain purchase is allowed only if any of the following conditions is met: 368 | * Customer has an existing product/service (`Pending` and `Terminated` don't count) 369 | * Customer is purchasing a domain and a product/service 370 | * `kt_onClientRegister` ordering one-off products is possible only for clients who registered within the last X number of days (`int`). Leave `false` to disable 371 | * `kt_promptRemoval` notify customer about restrictions via (previews are below): 372 | * `bootstrap-alert` right below *Review & Checkout* 373 | * `modal` on screen 374 | * `js-alert` on scren 375 | * `kt_textDisallowed` message displayed for product-based restriction 376 | * `kt_textRequireProduct` message displayed for domain-based resrticion 377 | 378 | When the hook detects that the customer is not allowed to order specific products/services and/or domains, it removes them from WHMCS cart showing alerts. 379 | 380 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-domain-require-product-one-off-products-2.png) 381 | 382 | The script highlights products/services used for promotions in green and with "promo" label. 383 | 384 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-offer-products-services-promo.png) 385 | 386 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/OneOffProductsDomainRequireProduct.php) 387 | 388 | ## New Clients as Affiliates 389 | 390 | Automatically sets newly registered customers as Affiliates on WHMCS. This way they don't need to join manually. 391 | 392 | That said, as you probably already know the affiliate system of WHMCS is very basic. If you need something more complete and sophisticated take a look at [Commission Manager](https://katamaze.com/whmcs/commission-manager/specifications). 393 | 394 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/NewClientsAsAffiliates.php) 395 | 396 | ## Send Email & Add Reply on Ticket Status Change 397 | 398 | When the status of a support ticket changes, WHMCS doesn't send any notification. We can tweak this process by sending an email and optionally also automatically add a reply to the ticket itself. This way you can guide customers through the resolving process letting them track the progress of tickets. 399 | 400 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/SendEmailAndAddReplyOnTicketStatusChange.php) 401 | 402 | ## Ticket Feedback on Auto Close via Escalation Rule 403 | 404 | Sending [feedback request](https://docs.whmcs.com/Support_Tab#Ticket_Closure_Feedback_Request) on ticket closure is a great way to measure customer support satisfaction however this feature has a missing piece. A ticket in WHMCS can be set as `Closed` in three different ways: 405 | 406 | * When the ticket is closed by an admin user 407 | * For inactivity `Setup > Automation Settings > Support Ticket Settings > Close Inactive Tickets` 408 | * Via [escalation rules](https://docs.whmcs.com/Support_Ticket_Escalations) 409 | 410 | WHMCS doesn't send any feedback request when the ticket is closed with an escalation rule. The hook in question solves this problem. The only requirements are the following: 411 | 412 | * `$cronFrequency` must be equal to your [System Cron Frequency](https://docs.whmcs.com/Crons#System_Cron) 413 | * You must use unique names for escalation rules 414 | 415 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/TicketFeedbackEscalationRule.php) 416 | 417 | ## Disable Feedback for Unanswered Tickets 418 | 419 | WHMCS always sends feedback requests for closed tickets, no matter what. This includes the case of customers closing tickets before you even had the chance to add a reply. For example a customer opens a ticket to report an error on his website. Few minutes later he realizes that it was his fault and closes the ticket. WHMCS still sends feedback request. 420 | 421 | This is quite strange as you are asking customers to let you know the *«Quality of experience»* with your support team. No one has even answered! Some customers could even give you a very bad rating because they feel you're tricking them. Let's patch the hole with this hook. 422 | 423 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/DisableFeedbackRequestsForUnansweredTickets.php) 424 | 425 | ## Client to Group based on Purchased Items 426 | 427 | Automatically assign customers to a client group based on purchased product/service, product addon and configurable options. It works only for customers that haven't been assigned to any group yet. Below we're going to show you how to define group/product pairs. Let's take this code as example. 428 | 429 | ``` 430 | $groups['products']['1'] = array('1', '2', '3'); 431 | $groups['products']['2'] = array('4'); 432 | $groups['productaddons']['1'] = array('2'); 433 | $groups['configurableoption']['3'] = array('5' => true, '6' => array('7', '8', '10')); 434 | ``` 435 | 436 | The key of the first level of `$groups` array (eg. `['products']`) can assume the following values: 437 | 438 | * `products` for group/product pairs 439 | * `productaddons` for group/product addon pairs 440 | * `configurableoption` for group/configurable option paris 441 | 442 | The key of the second level of `$groups` array (`['1']`, `['2']`) represents the client group ID. `array()` stores product IDs, product addon IDs and configurable options. Let's put it into practice explaining what the above configuration means: 443 | 444 | * Customer A purchases product `1`. He goes to client group ID `1` 445 | * Customer B purchases product `2`. He still goes to client group ID `1` 446 | * Customer C purchases product `4`. He goes to client group ID `2` 447 | * Customer D purchases product `5`. No action taken 448 | * Customer E purchases product `1` and is already assigned to a client group. No action taken 449 | * Customer F purchases product addon `2`. He goes to client group ID `1` 450 | * Customer G purchases a product selecting any value of configurable option ID `5`. He goes to client group ID `3` 451 | * Customer H purchases a product selecting specifically `7`, `8` or `10` options of configurable option ID `6`. He goes to client group ID `3` 452 | 453 | The script is available in two versions. The configuration is the same. What changes is the hook point: 454 | 455 | * [AcceptOrder version](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/AssignClientToGroupBasedOnPurchasedProduct_v1.php) assigns the group when the order is accepted - both manually and automatically 456 | * [EmailPreSend version](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/AssignClientToGroupBasedOnPurchasedProduct_v2.php) assign the group a moment before WHMCS sends the `Welcome Email` - any type (eg. Hosting, VPS, CodeGuard, Marketgoo...). This way the group just assigned is immediately ready for use in email templates 457 | 458 | ## Client to Group based on Registration Date 459 | 460 | This hook is similar to the one that [assigns clients to groups based on purchases](#client-to-group-based-on-purchased-items). This time we're assigning clients to groups based on registration date or more precisely on *user seniority*. Let's take this code as example. 461 | 462 | ``` 463 | $groups['1'] = '90'; 464 | $groups['2'] = '180'; 465 | $groups['3'] = '365'; 466 | ``` 467 | 468 | The key of `$groups` array (eg. `['1']`) represents the ID of the group while the value *user seniority* (days between registration date and current date). According to the above configuration, here is what happens: 469 | 470 | * Customer A registered `34` days ago. No change 471 | * Customer B registered `90` days ago. He goes to client group ID `2` 472 | * Customer C registered `364` days ago. Still group ID `2` 473 | * Customer D registered `500` days ago. He goes to client group ID `3` 474 | 475 | The hook runs with WHMCS daily cron job meaning that tomorrow the customer C of the above example will move from group `2` to `3`. Optionally, you can turn on any of the following features to add some restrictions: 476 | 477 | * `$activeCustomers` rules apply only on `Active` customers (boolean `true` or `false`) 478 | * `$oldestPurchase` rules apply only on if customer has a product/service or domain older than the given number of days (`integear`) 479 | * `$ignoreDomains` set `true` to ignore domain purchases when `$oldestPurchase` is in use 480 | * `$ignoreProducts` array of product IDs to ignore when `$oldestPurchase` is in use 481 | 482 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/AssignClientToGroupBasedOnRegistrationDate.php) 483 | 484 | ## Client to Group based on Registered Domains 485 | 486 | The hook assigns clients to groups based on the number of active domains in their accounts (`Active`, `Grace` and `Redemption`). This is particularly useful for [Domain Pricing slabs](https://docs.whmcs.com/Client_Groups#Domain_Pricing_Slabs). Let's take this code as example. 487 | 488 | ``` 489 | $groups['1'] = '10'; 490 | $groups['2'] = '25'; 491 | $groups['3'] = '100'; 492 | ``` 493 | 494 | The key of `$groups` array (eg. `['1']`) represents the ID of the group while the value the number of active domains. According to the above configuration, here is what happens: 495 | 496 | * Customer A has `10` domains. He goes to client group ID `2`. Next day domains become `9`. The the customer is removed from the group 497 | * Customer B has `99` domains. He still goes to client group ID `2` but will be moved to `3` in case he manages to reach `100` domains 498 | * Customer C has `250` domains. Group ID `3` 499 | 500 | The hook runs with WHMCS daily cron job meaning that customers are moved (or removed) from groups on a daily basis. Optionally, you can use the following feature to add some restrictions: 501 | 502 | * `$activeCustomers` rules apply only on `Active` customers (boolean `true` or `false`) 503 | * `$placeholderGroup` used to restrict assignments to a specific group (group ID or `false` to disable). This option requires further explanation as detailed below 504 | 505 | Let's assume we use the following configuration. 506 | 507 | ``` 508 | $groups['1'] = '10'; 509 | $groups['2'] = '25'; 510 | $groups['3'] = '100'; 511 | 512 | $placeholderGroup = '5'; 513 | ``` 514 | 515 | The hook processes assignments only on clients assigned to group ID `5` (the placeholder), `1`, `2` and `3`. Let's see some examples: 516 | 517 | * Customer A has `250` domains and is assigned to group `5`. After cron job he's moved to group `3` 518 | * Customer B has `10` domains and is assigned to group `1` and transfers away one domain. After cron job he's moved to group `5` as now he owns only `9` domains 519 | 520 | The placeholder can also be one of the existing group as in the following example: 521 | 522 | ``` 523 | $groups['1'] = '10'; 524 | $groups['2'] = '25'; 525 | $groups['3'] = '100'; 526 | 527 | $placeholderGroup = '1'; 528 | ``` 529 | 530 | In this case the `10` domains requirement for group `1` is ignored. The hook keeps track of changes to clients' group in the Activity Log. 531 | 532 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-move-clients-to-group-based-on-registered-domains.png) 533 | 534 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/AssignClientToGroupBasedOnRegisteredDomains.php) 535 | 536 | ## Exempt Existing Clients from Affiliate Commissions 537 | 538 | If a visitor places an order for a product or service with the [affiliation cookie](https://docs.whmcs.com/Affiliates) present, the affiliate earns a commission. The problem is that for WHMCS visitors and customers are the same thing meaning that it gives commissions to affiliates even for orders placed by existing customer. 539 | 540 | The hook prevents WHMCS from paying commission for customers registered on your from a given number of days. For example if you set `$numberOfDays = '30'`, WHMCS stops paying commissions for new orders placed by customers registered from more than 30 days on your site. 541 | 542 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/ExemptExistingClientsFromAffiliateCommissions.php) 543 | 544 | ## Prevent changes to Client Custom Fields 545 | 546 | WHMCS has an in-built function to lock client profile fields you want to prevent clients being able to edit from clientarea (eg. email, company name). This feature however is not avaiable for client custom fields. Making such fields "disabled" via HTML is not an option. Anyone with bit of HTML knowledge can skip this form of protection. 547 | 548 | This hook acts as the last line of defense. It grants that no customer can submit changes. If necessary it can be enabled also for WHMCS Administrators. 549 | 550 | If you need something more professional, [Billing Extension](https://katamaze.com/whmcs/billing-extension) can bring your WHMCS to the next level with things like [monthly invoicing](https://katamaze.com/docs/billing-extension/4/reducing-the-number-of-invoices), electronic invoicing, [customer retention](https://katamaze.com/docs/billing-extension/39/client-area#Customer-Retention), [Facebook Pixel](https://katamaze.com/docs/billing-extension/43/facebook-pixel) and much more. 551 | 552 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/PreventChangesToClientCustomFields.php) 553 | 554 | ## Quote to Invoice conversion without redirect 555 | 556 | If you are sending out a lot of quotes on a daily basis, the fact that WHMCS forces a redirect to the newly issued invoice could be frustrating. This hook prevents WHMCS from performing the redirect allowing you to keep woriking on the quote. 557 | 558 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/QuoteToInvoiceNoRedirect.php) 559 | 560 | ## Remove/Hide Breadcrumb 561 | 562 | WHMCS prepends *Portal Home* to breadcrumb. There's nothing wrong with that but some people don't like it. This hook removes it from all WHMCS pages. 563 | 564 | Bonus tip: if you don't want to use an action hook, you can use the following CSS. The result is the same. 565 | 566 | ``` 567 | .breadcrumb li:first-child { 568 | display:none; 569 | } 570 | .breadcrumb li:nth-child(2):before { 571 | content:" "; 572 | } 573 | ``` 574 | 575 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/RemovePortalHomeBreadcrumb.php) 576 | 577 | ## Knowledgebase Author 578 | 579 | WHMCS doesn't store any information about author - the administrator who published a KB article. This hook automatically adds a new column in `tblknowledgebase` named `kt_author` (the `kt_` prefix is important to avoid naming collision). When an admin adds an article to KB the same hook stores author information and shows it on screen. 580 | 581 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-author-knowledgebase-article.png) 582 | 583 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/KnowledgebaseAuthor.php) 584 | 585 | ## Knowledgebase Last Updated Date 586 | 587 | WHMCS doesn't store *Last Updated* date when you edit Knowledgebase articles but you can retreive from Activity Log. It's not a stylish solution but it works. The hook adds `lastupdated` element to the existing `$kbarticle` Smarty array. Once done, change your KB template accordingly. 588 | 589 | If you're looking for something more professional and up to date, learn how to benefit from [WHMCS SEO](https://katamaze.com/blog/37/whmcs-seo-ways-to-improve-your-site-ranking-in-2020) using [WHMCS as CMS](https://katamaze.com/whmcs/mercury). 590 | 591 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/KnowledgebaseLastUpdatedDate.php) 592 | 593 | ## Login as Client Language 594 | 595 | Every time an administrator uses *Login as Client*, WHMCS overrides the default language of the selected customer with the one used by the administrator in WHMCS backend. This is bad because you're unknowingly changing the default language for your customer. This also applies for languages that can't be used in clientarea. 596 | 597 | Let's say your clientarea is in italian and you're using WHMCS backend in english. When you perform the *Login as Client*, WHMCS switches customer's language from italian to english and there's no way back. The customer in question is stucked with a language he cannot change. The following hook prevents that to happen. 598 | 599 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/LoginAsClientPreserveLanguage.php) 600 | 601 | ## Prevent Emails to be sent based on Client Group 602 | 603 | The hook prevents WHMCS from sending *General Messages* email templats to specific client groups based on a sort of blacklist. 604 | 605 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/PreventEmailSendingBasedOnClientGroup.php) 606 | 607 | ## Abort Auto-Provisioning when there's a Note in the Order 608 | 609 | A customer orders a VPS and adds notes to request a particular configuration that requires your manual intervention. In case you're using auto-provisioning, there's no way to stop WHMCS from creating the VPS to let you intervene manually. This hook however can stop auto-provisioning when there's a note in the order. 610 | 611 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/NoteInTheOrderAbortAutoProvisioning.php) 612 | 613 | ## Rename Addon Module Label 614 | 615 | Let's say you don't like how we named [Mercury](https://katamaze.com/whmcs/mercury/specifications), [Commission Manager](https://katamaze.com/whmcs/commission-manager/specifications), [Billing Extension](https://katamaze.com/whmcs/billing-extension/specifications) addon modules and you want to change them to *CMS*, *Affiliates* and *Accounting*. Simply change the following hook accordingly. It works with any WHMCS module. 616 | 617 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-rename-addon-module-name-label-menu.png) 618 | 619 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/RenameAddonModuleLabel.php) 620 | 621 | ## Add Button next to Module's Functions 622 | 623 | Here is how you can add a button next to *Create*, *Suspend*, *Unsuspend* (...) functions in product/service view. 624 | 625 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-add-button-next-to-module-function-create-change-password.png) 626 | 627 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/AddButtonNextToModulesFunctions.php) 628 | 629 | ## Announcements Meta Description 630 | 631 | Before you think *«Great! I can finally add meta descriptions to WHMCS announcements»* wait for a sec and understand the following: 632 | 633 | * [WHMCS is terrible at SEO](https://katamaze.com/blog/37/whmcs-seo-ways-to-improve-your-site-ranking-in-2020). You need more than an hook to improve rankings 634 | * [Meta Description](https://katamaze.com/blog/37/whmcs-seo-ways-to-improve-your-site-ranking-in-2020#Meta-description) is **not** a ranking factor. It doesn't affect your rankings but CTR 635 | 636 | You can use the same approach to implement other meta tags but stay away from [meta keywords](https://katamaze.com/blog/37/whmcs-seo-ways-to-improve-your-site-ranking-in-2020#Meta-keywords). It is useless and has been deprecated more than a decade ago by all search engines. 637 | 638 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/AnnuncementsMetaDescription.php) 639 | 640 | ## Prevent Indexing on Search Engines 641 | 642 | With this hook you can stop search engines from indexing your WHMCS site. This is particularly useful for test installations and for sites you still need to launch. The hook adds `noindex` meta tag in the `` of your WHMCS that tells search engine crawlers to not index pages. 643 | 644 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/PreventSearchEngineIndexing.php) 645 | 646 | ## Promotion Code in Email Template 647 | 648 | *Invoice Payment Confirmation* is an Email Template that WHMCS sends to customers when they pay invoices. By default this message doesn't include any information about promotions. The following hook add coupon code to the invoice recepit (if a promo has been applied). 649 | 650 | Once the hook has been added to WHMCS, you can edit *Invoice Payment Confirmation* email template to customize the look of your message like follows. 651 | 652 | ``` 653 | {if $assigned_promos} 654 | Promo below: 655 | {foreach from=$assigned_promos item=promo} 656 | {$promo} 657 | {/foreach} 658 | {/if} 659 | ``` 660 | 661 | Here is a preview of the message. 662 | 663 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-promotion-code-invoice-payment-confirmation.png) 664 | 665 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hooks/blob/master/hooks/CouponCodeInEmailTemplate.php) 666 | 667 | ## Promotions array in Email Templates 668 | 669 | This hook is capable of including information about existing promotions (aka coupon codes) in any email notifications sent by WHMCS. It adds `{$promotions}` Smarty array to any of the specified email templates. You only need to iterate records with a Smarty `{foreach}` as follows. 670 | 671 | ``` 672 | {if $promotions} 673 | Active Promotions: 674 | 679 | {/if} 680 | ``` 681 | 682 | Here's a preview of the following code. Keep in mind that the hook automatically removes expired promotions from the array. 683 | 684 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-promotions-coupon-in-email-templates.png) 685 | 686 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/PromotionsArrayInEmailTemplates.php) 687 | 688 | ## Automatically Accept Order when Invoice is Paid 689 | 690 | WHMCS requires administrators to manually accept orders even if automation tasks already took place. This hook automatically accepts orders via API when Invoice is paid. 691 | 692 | * Restrict order acceptance based on `$invoiceTotal`. The script automatically performs currency conversion. Leave `false` to auto-accept everything 693 | * Use `<=` as `$operator` to auto-accept orders less than or equal to `$invoiceTotal`. Use `>=` for the opposite 694 | 695 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/AcceptOrderOnInvociePaid.php) 696 | 697 | ## Cancel Order when an Invoice is being Cancelled 698 | 699 | When an invoice is `Mark Cancelled` the related order (if exists) is automatically set `Cancelled`. 700 | 701 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/CancelOrderOnInvoiceCancelled.php) 702 | 703 | ## Ban Order Expiration 704 | 705 | WHMCS administrators can ban IP on the fly directly from order view. The problem with this feature is that WHMCS automatically sets the ban to expire the last day of current year. As you can imagine there's an huge difference between receiving the ban on January instead of December. This hook makes sure that bans always last one full year no matter what. 706 | 707 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/BanOrderExpiration.php) 708 | 709 | ## Hide Google Invisible reCAPTCHA Badge 710 | 711 | All it takes to hide Google Invisible reCAPTCHA Badge (bottom-right corner) is a CSS rule. If you don't want to edit your CSS and/or want preserve the change with template updates, use this hook. Before you ask, yes, the correct way to hide the Badge is to use `opacity`. Using things like `display: none` and `visibility: hidden` breaks reCAPTCHA. 712 | 713 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/HideGoogleInvisibleReCAPTCHA.php) 714 | 715 | ## Chatstack Disable for Logged-In Users and Administrators 716 | 717 | In case you have now idea of what Chatstack is, let me give you a little bit of background. It's an official module of WHMCS that allows to chat with visitors and track their activities. We use it ourselves on [our site](https://katamaze.com/). It's the little badge at the bottom right corner. Visitors can click it to start chatting with us. In case we're not online, the badge redirects to *contact us*. 718 | 719 | It is worth to mention that in past Chatstack was named LiveHelp. You can purchase it directly from [Chatstack](https://www.chatstack.com/) or from WHMCS [Marketplace](https://marketplace.whmcs.com/product/34-live-chat-visitor-tracking). Ignore all the negative reviews. Most of them are from people that have no idea of how to install and configure it 😑 720 | 721 | Let's now move to the hook itself. Once Chatstack is installed on your WHMCS site, it starts tracking everyone including WHMCS administrators and logged-in users. This creates the following problems: 722 | 723 | * You receive notifications about administrators' activities 724 | * Chatstack doesn't distinguish between visitors and administrators 725 | * Chat should be reserved for pre-sales but customers can use it for technical support 726 | 727 | The hook we made provides two options that allows to: 728 | 729 | * Stop tracking and notifying administrators' activities 730 | * Prevent logged-in users (existing customers) to use the chat 731 | 732 | The only requirement is that you remove any existing integration between WHMCS & Chatstack. The action hook handles everything and supports also [WHMCS multi-domain and multi-brand](https://katamaze.com/docs/mercury/48/multi-brand-and-geolocation#Multi-brand-and-multi-domain). 733 | 734 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/ChatstackDisableLoggedInAndAdmin.php) 735 | 736 | ## Notify Fradulent Orders 737 | 738 | When an order is set as fraud, prior to the change of status actually occurring, the hook sends email notifications to all existing WHMCS administrators (disabled administrators are ignored). 739 | 740 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/hooks/NotifyFradulentOrders.php) 741 | 742 | ## Conditional Support Departments 743 | 744 | Restrict the access to support departments based on the products purchased by users. Define rules as follows. 745 | 746 | ``` 747 | $department['1'] = array('45', '46', '10'); 748 | $department['2'] = array('85', '86', '10'); 749 | // Keep adding rules one per line 750 | ``` 751 | 752 | The key of `$department` array (the `[1]` and `[2]` between square brackets) corresponds to the ID of the support department for which we are creating a rule. The value is an `array()` of product IDs required for access. In a in nutshell, the above configuration unlocks department `#1` to users with product IDs `45`, `46` and `10`. Department `#2` requires `85`, `86` and `10`. 753 | 754 | Here are few more things to consider: 755 | 756 | * `submitticket.php` doesn't show restricted departments 757 | * Access via direct link `submitticket.php?step=2&deptid=2` triggers a redirect to `submitticket.php` 758 | * Department dropdown lists only allowed department 759 | * The same product can be used for multiple rules 760 | * `Pending`, `Suspended`, `Terminated`, `Cancelled` and `Fraud` products are ignored 761 | 762 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Scripts/blob/master/hooks/ConditionalSupportDepartments.php) 763 | 764 | ## Abort Email Sending based on User ID and/or Client Group ID 765 | 766 | Let's take as example the following configuration: 767 | 768 | ``` 769 | $disallowedEmailTemplates = array('Invoice Created'); 770 | $disallowedClientGroups = array('3'); 771 | $disallowedUserIDs = array('1'); 772 | $removePDFAttachments = true; 773 | ``` 774 | 775 | We are aborting the sending of `Invoice Created` email to client ID `1` and also to clients assigned to client group `3`. You can specify multiple email templates, client groups and user ID - all these parameters are `array()`. The `$removePDFAttachments` can be used to simply remove PDF invoice attachments from emails. 776 | 777 | * `$disallowedEmailTemplates` the system name of the email template that can be found in `Setup > Email Templates`. When you edit a template, the system name appears right below `Email Templates` title 778 | * `$disallowedClientGroups` an array of client group IDs that can be found in `Setup > Client Groups` 779 | * `$disallowedUserIDs` an array of user IDs 780 | * `$removePDFAttachments` set `true` if you simply want to remove PDF invoice attachments for the selected users 781 | 782 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Scripts/blob/master/hooks/AbortEmailSendingBasedOnUserIDClientGroupID.php) 783 | 784 | ## Generate Missing UUID in tblclients 785 | 786 | Importing clients in `tblclients` table via queries or from phpMyAdmin, does not automatically create `uuid` values. This script will generate `uuid` for clients that don't have one yet. It triggers by visiting any page of frontend. Don't forget to remove the it when you finished. 787 | 788 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hook-Factory/blob/master/hooks/GenerateUUID.php) 789 | 790 | ## Change Default Sorting of Tables in Backend 791 | 792 | It feels weird when you open `Billing > Invoices` and WHMCS sorts records by `Due Date`. A more convenient way to sort invoices is by `Invoice Date`. It took me plenty of time to figure out how to change default sorting for tables in backend. 793 | 794 | WHMCS stores sorting order for each page in the mysterious `$_COOKIE['WHMCSSD']` array. Why am I saying mysterious? Because for reasons I can't understand, WHMCS staff decided to `json_encode` and `base64_encode` its content. That's why nobody before me knew this secret. 795 | 796 | [Get the Code »](https://github.com/Katamaze/WHMCS-Action-Hook-Factory/blob/master/hooks/ChangeDefaultSortingBackendTables.php) 797 | 798 | # Free Reports Collection 799 | 800 | Yay! We didn't stop to action hooks :stuck_out_tongue: Below you can find a list of custom [WHMCS Reports](https://docs.whmcs.com/Reports) to give you more in-depth reporting and analytics on the performance of your business. Let's go! 801 | 802 | ## Churn Rate 803 | 804 | Rate at which customers stop doing business with you. The report includes charts and graphs to help you interpret the data. For every month of the year you can see: 805 | 806 | * No. of products/domains at the start of the month 807 | * No. of products/domains at the end of the month 808 | * Monthly change in the No. of products/domains 809 | * No. of products/domains acquired during the month 810 | * No. of products/domains lost during the month 811 | * Churn rate (percentage) 812 | 813 | The report also shows cumulative statistics (products & domains combined). Churn rate is usually connected to [customer retention](https://katamaze.com/docs/billing-extension/39/client-area#Customer-Retention). The linked article describes how to retain customers. For your information the formula to calculate churn rate is the following. 814 | 815 | ``` 816 | (Lost products/domains at the end of Time Period / Acquired products/domains at the end of Time Period) * 100 817 | ``` 818 | 819 | The report doesn't take into account products/services with any of the following billing cycles: `One Time`, `Completed`, `Free Account`. The reason for that is very simple. Such products don't support renewals hence churn rate doesn't apply. 820 | 821 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-churn-rate-report-3.png) 822 | 823 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/reports/Churn_Rate.php) 824 | 825 | # Free Modules Collection 826 | 827 | Yep, we also give you some handy modules that can help you with most common operations in WHMCS. 828 | 829 | ## Plesk Checker 830 | 831 | > Error code: 1013. Error message: Customer with external id 'whmcs_plesk_XX' is not found in panel. 832 | 833 | If you're using Plesk I bet you've seen this error at least once in your life. If you try to google it, you'll find a page from [Plesk documentation](https://support.plesk.com/hc/en-us/articles/115002988474-Cannot-login-to-Plesk-via-WHMCS-after-migration-Customer-with-external-id-whmcs-plesk-XX-is-not-found-in-panel) that claims this bug (I quote) *«has been already fixed for all versions»*. I respectfully disagree. 834 | 835 | Probably I'll need a week to explain why this error appears. Let's skip this boring part. Fixing the error requires your manual intervention on `psa` database (Plesk) and more in particular on `clients.external_id` column as described in the article previously linked. 836 | 837 | We created a module that makes this "find and replace" process less frustrating and quicker. Not only it automatically detects all hosting accounts that are returing `Error code: 1013` but also additional ones. Here's a preview. 838 | 839 | ![image](https://katamaze.com/modules/addons/Mercury/uploads/files/Blog/92b1487d05bc7249c65af0f94cde4732/whmcs-plesk-checker-external-id-2.png) 840 | 841 | [Get the Code »](https://github.com/Katamaze/WHMCS-Free-Action-Hooks/tree/master/modules/addons/PleskChecker) 842 | 843 | # Tweaks 844 | 845 | # Client area Domain List 846 | 847 | According to WHMCS sidebar there are say 8 expired domains but when you apply this filter, the table shows more than 8 records. What is going on? That's simple. WHMCS countings are inconsistent. The sidebar counts expired domains while the table includes in this counting also cancelled domains. 848 | 849 | You can easily solve the problem by editing `clientareadomains.tpl`. Find `{if $domain.expiringSoon}` and replace with `{if $domain.expiringSoon AND $domain.statusClass != 'cancelled'}`. 850 | -------------------------------------------------------------------------------- /hooks/AbortEmailSendingBasedOnUserIDClientGroupID.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use PHPMailer\PHPMailer\PHPMailer; 13 | use PHPMailer\PHPMailer\Exception; 14 | use WHMCS\Database\Capsule; 15 | 16 | add_hook('EmailPreSend', 1, function($vars) 17 | { 18 | $disallowedEmailTemplates = array('Invoice Created'); // The name of the email template being sent 19 | $disallowedClientGroups = array('3'); // Affected Client Group ID 20 | $disallowedUserIDs = array('5'); // Affected User ID 21 | $removePDFAttachments = true; // Set true if you simply want to remove PDF invoice attachments for the selected users 22 | 23 | switch (Capsule::table('tblemailtemplates')->select('type')->where('name', $vars['messagename'])->first()->type) 24 | { 25 | case 'affiliate': $user = $vars['relid']; break; 26 | case 'domain': $user = Capsule::select(Capsule::raw('SELECT t2.id, t2.groupid FROM tbldomains AS t1 LEFT JOIN tblclients AS t2 ON t1.userid = t2.id WHERE t1.id = "' . $vars['relid'] . '" LIMIT 1'))[0]; break; 27 | case 'general': $user = $vars['relid']; break; 28 | case 'invoice': $attachments = true; $user = Capsule::select(Capsule::raw('SELECT t2.id, t2.groupid, t2.language FROM tblinvoices AS t1 LEFT JOIN tblclients AS t2 ON t1.userid = t2.id WHERE t1.id = "' . $vars['relid'] . '" LIMIT 1'))[0]; break; 29 | case 'product': $user = Capsule::select(Capsule::raw('SELECT t2.id, t2.groupid FROM tblhosting AS t1 LEFT JOIN tblclients AS t2 ON t1.userid = t2.id WHERE t1.id = "' . $vars['relid'] . '" LIMIT 1'))[0]; break; 30 | case 'support': $user = Capsule::select(Capsule::raw('SELECT t2.id, t2.groupid FROM tbltickets AS t1 LEFT JOIN tblclients AS t2 ON t1.userid = t2.id WHERE t1.id = "' . $vars['relid'] . '" LIMIT 1'))[0]; break; 31 | default: return; break; 32 | } 33 | 34 | if (in_array($user->groupid, $disallowedClientGroups) OR in_array($user->id, $disallowedUserIDs)) 35 | { 36 | $abortSend = true; 37 | } 38 | 39 | if ($removePDFAttachments AND $attachments AND $abortSend) 40 | { 41 | require __DIR__ . '/../../vendor/phpmailer/phpmailer/src/Exception.php'; 42 | require __DIR__ . '/../../vendor/phpmailer/phpmailer/src/PHPMailer.php'; 43 | require __DIR__ . '/../../vendor/phpmailer/phpmailer/src/SMTP.php'; 44 | 45 | foreach (Capsule::select(Capsule::raw('SELECT setting, value FROM tblconfiguration WHERE setting IN ("CompanyName", "Email", "MailType", "SMTPHost", "SMTPUsername", "SMTPPassword", "SMTPPort", "SMTPSSL")')) as $v) 46 | { 47 | if ($v->setting == 'SMTPPassword' AND $v->value): $v->value = Decrypt($v->value); endif; 48 | $conf[$v->setting] = $v->value; 49 | } 50 | 51 | $mail = new PHPMailer; 52 | $mail->CharSet = 'UTF-8'; 53 | 54 | try 55 | { 56 | if ($conf->MailType == 'smtp') 57 | { 58 | $mail->IsSMTP(); 59 | $mail->Host = $conf['SMTPHost']; 60 | $mail->SMTPAuth = true; 61 | $mail->SMTPSecure = $conf['SMTPSSL']; 62 | $mail->Port = $conf['SMTPPort']; 63 | $mail->Username = $conf['SMTPUsername']; 64 | $mail->Password = $conf['SMTPPassword']; 65 | $mail->Mailer = 'smtp'; 66 | $mail->CharSet = 'UTF-8'; 67 | } 68 | else 69 | { 70 | $mail->IsMail(); 71 | $mail->CharSet = 'UTF-8'; 72 | } 73 | 74 | $emailTemplate = Capsule::select(Capsule::raw('SELECT subject, message FROM tblemailtemplates WHERE name = "' . $vars['messagename'] . '" AND language = "' . $user->language . '" LIMIT 1'))[0]; 75 | 76 | foreach (array('invoice_html_contents', 'client_name', 'invoice_date_created', 'invoice_payment_method', 'invoice_num', 'invoice_total', 'invoice_date_due', 'signature') as $v) 77 | { 78 | $emailTemplate->subject = str_replace('{$' . $v . '}', $vars['mergefields'][$v], $emailTemplate->subject); 79 | $emailTemplate->message = str_replace('{$' . $v . '}', $vars['mergefields'][$v], $emailTemplate->message); 80 | } 81 | 82 | $mail->AddAddress($vars['mergefields']['client_email'], $vars['mergefields']['client_name']); 83 | $mail->SetFrom($conf['Email'], $conf['CompanyName']); 84 | $mail->Subject = $emailTemplate->subject; 85 | $mail->MsgHTML($emailTemplate->message); 86 | $mail->Send(); 87 | $mail->ClearAllRecipients(); 88 | } 89 | catch (phpmailerException $e) 90 | { 91 | //echo $e->errorMessage(); // Pretty error 92 | } 93 | catch (Exception $e) 94 | { 95 | //echo $e->getMessage(); // Boring error 96 | } 97 | } 98 | 99 | if ($abortSend) 100 | { 101 | return array('abortsend' => true); 102 | } 103 | }); 104 | -------------------------------------------------------------------------------- /hooks/AcceptOrderOnInvociePaid.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('InvoicePaid', 1, function($vars) { 15 | 16 | $orderID = Capsule::table('tblorders')->where('invoiceid', '=', $vars['invoiceid'])->pluck('id')[0]; 17 | if (!$orderID): return; endif; 18 | 19 | $invoiceTotal = '10'; // Auto-accept order based on invoice total. The script automatically performs currency conversion. Leave false to auto-accept everything 20 | $operator = '<='; // Use "<=" to auto-accept orders less than or equal to $invoiceTotal. Use ">=" for the opposite 21 | 22 | if ($invoiceTotal) { 23 | 24 | $currency = Capsule::select(Capsule::raw('SELECT t3.rate FROM tblinvoices AS t1 LEFT JOIN tblclients AS t2 ON t1.userid = t2.id LEFT JOIN tblcurrencies AS t3 ON t2.currency = t3.id WHERE t1.id = "' . $vars['invoiceid'] . '" AND t3.default = "0" LIMIT 1'))[0]; 25 | $invoiceTotal = ($currency ? $invoiceTotal * $currency->rate : $invoiceTotal); 26 | 27 | if (Capsule::select(Capsule::raw('SELECT id FROM tblinvoices WHERE id = "' . $vars['invoiceid'] . '" AND (total ' . ($operator == '>=' ? '<=' : '>=') . ' "' . $invoiceTotal . '" OR credit ' . ($operator == '>=' ? '<=' : '>=') . ' "' . $invoiceTotal . '") LIMIT 1'))[0]) { 28 | 29 | return; 30 | } 31 | } 32 | 33 | $adminUsername = ''; // Optional for WHMCS 7.2 and later 34 | localAPI('AcceptOrder', array('orderid' => $orderID), $adminUsername); 35 | }); 36 | 37 | add_hook('AfterProductUpgrade', 1, function($vars) { 38 | 39 | $orderID = Capsule::table('tblupgrades')->where('id', '=', $vars['upgradeid'])->pluck('orderid')[0]; 40 | if (!$orderID): return; endif; 41 | 42 | $invoiceTotal = '10'; // Auto-accept order based on invoice total. The script automatically performs currency conversion. Leave false to auto-accept everything 43 | $operator = '<='; // Use "<=" to auto-accept orders less than or equal to $invoiceTotal. Use ">=" for the opposite 44 | 45 | if ($invoiceTotal) { 46 | 47 | $currency = Capsule::select(Capsule::raw('SELECT t3.rate FROM tblinvoices AS t1 LEFT JOIN tblclients AS t2 ON t1.userid = t2.id LEFT JOIN tblcurrencies AS t3 ON t2.currency = t3.id WHERE t1.id = "' . $vars['invoiceid'] . '" AND t3.default = "0" LIMIT 1'))[0]; 48 | $invoiceTotal = ($currency ? $invoiceTotal * $currency->rate : $invoiceTotal); 49 | 50 | if (Capsule::select(Capsule::raw('SELECT t2.id FROM tblorders AS t1 LEFT JOIN tblinvoices AS t2 ON t1.invoiceid = t2.id WHERE t1.id = "' . $orderID . '" AND (t2.total ' . ($operator == '>=' ? '<=' : '>=') . ' "' . $invoiceTotal . '" OR t2.credit ' . ($operator == '>=' ? '<=' : '>=') . ' "' . $invoiceTotal . '") LIMIT 1'))[0]) { 51 | 52 | return; 53 | } 54 | } 55 | 56 | $adminUsername = ''; // Optional for WHMCS 7.2 and later 57 | localAPI('AcceptOrder', array('orderid' => $orderID), $adminUsername); 58 | }); 59 | -------------------------------------------------------------------------------- /hooks/AcceptQuoteWithoutLogin.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | */ 12 | 13 | use WHMCS\Database\Capsule; 14 | 15 | add_hook('EmailPreSend', 1, function($vars) 16 | { 17 | if (in_array($vars['messagename'], array('Quote Delivery with PDF'))) 18 | { 19 | if ($vars['mergefields']['quote_link']) 20 | { 21 | $data = Capsule::select(Capsule::raw('SELECT t1.id, t2.id AS clientid, t2.email FROM tblquotes AS t1 LEFT JOIN tblclients AS t2 ON t1.userid = t2.id WHERE t1.id = "' . $vars['mergefields']['quote_number'] . '" LIMIT 1'))[0]; 22 | 23 | $hash = strrev(md5($data->id . $data->clientid . $data->email)) . '-' . $data->id; 24 | $quote_link = (new SimpleXMLElement($vars['mergefields']['quote_link']))['href']; 25 | $url = parse_url($quote_link); 26 | $merge_fields['quote_link'] = str_replace($quote_link, $url['scheme'] . '://' . $url['host'] . '/index.php?qhash=' . $hash, $vars['mergefields']['quote_link']); 27 | 28 | return $merge_fields; 29 | } 30 | } 31 | }); 32 | 33 | add_hook('ClientAreaHeadOutput', 1, function($vars) 34 | { 35 | if ($_GET['qhash']) 36 | { 37 | $data = Capsule::select(Capsule::raw('SELECT t1.id, t1.subject, t2.id AS clientid, t2.firstname, t2.email FROM tblquotes AS t1 LEFT JOIN tblclients AS t2 ON t1.userid = t2.id WHERE t1.id = "' . explode('-', $_GET['qhash'])[1] . '" AND stage != "Accepted" LIMIT 1'))[0]; 38 | $hash = strrev(md5($data->id . $data->clientid . $data->email)) . '-' . $data->id; 39 | 40 | if ($hash === $_GET['qhash']) 41 | { 42 | $adminUsername = ''; // Optional for WHMCS 7.2 and later 43 | $results = localAPI('AcceptQuote', array('quoteid' => $data->id), $adminUsername); 44 | $results = localAPI('SendEmail', array('messagename' => 'Invoice Created', 'id' => $results['invoiceid']), $adminUsername); 45 | 46 | return << 48 | setTimeout(function() 49 | { 50 | $("#modalAjax .modal-title").html('Quote #{$data->id} Accepted'); 51 | $("#modalAjax .modal-body").html('

Hey, {$data->firstname}

Thanks for accepting quote #{$data->id} ({$data->subject}). Here is what happens now:

  • You will receive the invoice shortly
  • Once we receive your payment, we\'ll activate your order
Please do not hesitate to contact us if you have any questions.

Keep browsing our Products

Discover

'); 52 | $('#modalAjax .loader').hide(); 53 | $('#modalAjax .modal-submit').hide(); 54 | $("#modalAjax").modal('show'); 55 | }, 250); 56 | 57 | HTML; 58 | } 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /hooks/AddButtonNextToModulesFunctions.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | add_hook('AdminAreaHeaderOutput', 1, function($vars) 13 | { 14 | return << 16 | $(document).ready(function(){ 17 | $("#modcmdbtns").append(''); 18 | }); 19 | 20 | HTML; 21 | }); 22 | -------------------------------------------------------------------------------- /hooks/AdminStatsForWHMCSv8.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('AdminAreaHeaderOutput', 1, function($vars) 15 | { 16 | $v8 = Capsule::select(Capsule::raw('SELECT value FROM tblconfiguration WHERE setting = "Version" LIMIT 1'))[0]->value; 17 | if (explode('.', $v8)[0] != '8'): return; endif; 18 | $showZero = true; 19 | 20 | $ordersTotal = Capsule::select(Capsule::raw('SELECT COUNT(t1.id) AS total FROM tblorders AS t1 LEFT JOIN tblorderstatuses AS t2 ON t1.status = t2.title WHERE t2.showpending = "1"'))[0]->total; 21 | $invoicesTotal = Capsule::select(Capsule::raw('SELECT COUNT(id) AS total FROM tblinvoices WHERE status = "Unpaid" AND duedate <= CURDATE()'))[0]->total; 22 | $ticketsTotal = Capsule::select(Capsule::raw('SELECT COUNT(t1.id) AS total FROM tbltickets AS t1 LEFT JOIN tblticketstatuses AS t2 ON t1.status = t2.title WHERE t2.showawaiting = "1" AND merged_ticket_id = "0"'))[0]->total; 23 | if (!$ordersTotal AND !$invoicesTotal AND !$ticketsTotal): return; endif; 24 | $notificationsLabel = AdminLang::trans('setup.notifications'); 25 | $orderText = AdminLang::trans('stats.pendingorders'); 26 | $invoiceText = AdminLang::trans('stats.overdueinvoices'); 27 | $ticketText = AdminLang::trans('stats.ticketsawaitingreply'); 28 | 29 | if ($ordersTotal OR $showZero) 30 | { 31 | $pendingOrdersJS = <<{$ordersTotal} {$orderText}'); 33 | HTML; 34 | } 35 | 36 | if ($invoicesTotal OR $showZero) 37 | { 38 | $overdueInvoicesJS = <<{$invoicesTotal} {$invoiceText}'); 40 | HTML; 41 | } 42 | 43 | if ($ticketsTotal OR $showZero) 44 | { 45 | $awaitingTicketsJS = <<{$ticketsTotal} {$ticketText}'); 47 | HTML; 48 | } 49 | 50 | return << 52 | $(document).on('ready', function() { 53 | $('ul.right-nav').first('li').prepend('
  •  {$notificationsLabel}
  • '); 54 | $("*[id=\'v8fallback\']").on("click", function(e) { 55 | e.preventDefault(); 56 | $(e.currentTarget).parent("li").toggleClass("expanded"); 57 | }); 58 | {$pendingOrdersJS} 59 | {$overdueInvoicesJS} 60 | {$awaitingTicketsJS} 61 | $('#v8fallback').next('ul').css({"width": "340px", "left": "-134px"}); 62 | $('span.v8fallback').css({"font-weight": "700"}); 63 | }); 64 | 65 | HTML; 66 | }); 67 | -------------------------------------------------------------------------------- /hooks/AnnuncementsMetaDescription.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | add_hook('ClientAreaHeadOutput', 1, function($vars) 13 | { 14 | if ($vars['templatefile'] == 'viewannouncement') 15 | { 16 | return << 18 | HTML; 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /hooks/AssignClientToGroupBasedOnPurchasedProduct_v1.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('AcceptOrder', 1, function($vars) 15 | { 16 | // Define group/product pairs. Instructions provided below 17 | // https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/README.md#client-to-group-based-on-purchased-productservice 18 | $groups['products']['1'] = array('1', '2', '3'); 19 | $groups['products']['2'] = array('4'); 20 | $groups['productaddons']['1'] = array('2'); 21 | $groups['configurableoption']['1'] = array('5' => array('7', '8', '10'), '6' => true); 22 | 23 | if (!$groups): return; endif; 24 | $userID = Capsule::table('tblorders')->where('id', $vars['orderid'])->pluck('userid')[0]; 25 | $orderedProducts = Capsule::table('tblhosting')->where('orderid', $vars['orderid'])->pluck('packageid', 'id'); 26 | $orderedProductAddons = Capsule::table('tblhostingaddons')->where('orderid', $vars['orderid'])->pluck('addonid'); 27 | 28 | if ($groups['configurableoption']) 29 | { 30 | foreach (Capsule::select(Capsule::raw('SELECT t1.relid, t1.configid, t1.optionid, t1.qty, t2.optiontype FROM tblhostingconfigoptions as t1 LEFT JOIN tblproductconfigoptions AS t2 ON t1.configid = t2.id LEFT JOIN tblproductconfigoptionssub AS t3 ON t1.optionid = t3.id WHERE t1.relid IN (\'' . implode('\',\'', array_keys($orderedProducts)) . '\')')) as $v) 31 | { 32 | $relid = $v->relid; 33 | $configid = $v->configid; 34 | 35 | if (in_array($v->optiontype, array('3', '4'))) 36 | { 37 | $value = ($v->qty ? true : false); 38 | } 39 | else 40 | { 41 | $value = $v->optionid; 42 | } 43 | 44 | unset($v); 45 | 46 | if ($value) 47 | { 48 | $orderedConfigurableOptions[$relid][$configid] = $value; 49 | } 50 | } 51 | } 52 | 53 | foreach ($groups['products'] as $group => $target) 54 | { 55 | if (array_intersect($orderedProducts, $target)) 56 | { 57 | Capsule::table('tblclients')->where('id', $userID)->where('groupid', '0')->update(['groupid' => $group]); 58 | return; 59 | } 60 | } 61 | 62 | foreach ($groups['productaddons'] as $group => $target) 63 | { 64 | if (array_intersect($orderedProductAddons, $target)) 65 | { 66 | Capsule::table('tblclients')->where('id', $userID)->where('groupid', '0')->update(['groupid' => $group]); 67 | return; 68 | } 69 | } 70 | 71 | foreach ($groups['configurableoption'] as $group => $configurableOptions) 72 | { 73 | foreach ($configurableOptions as $configID => $options) 74 | { 75 | if (is_array($options)) 76 | { 77 | foreach ($orderedConfigurableOptions as $target) 78 | { 79 | if (array_intersect($options, $target)) 80 | { 81 | Capsule::table('tblclients')->where('id', $userID)->where('groupid', '0')->update(['groupid' => $group]); 82 | return; 83 | } 84 | } 85 | } 86 | else 87 | { 88 | foreach ($orderedConfigurableOptions as $target) 89 | { 90 | if (in_array($configID, $target)) 91 | { 92 | Capsule::table('tblclients')->where('id', $userID)->where('groupid', '0')->update(['groupid' => $group]); 93 | return; 94 | } 95 | } 96 | } 97 | } 98 | } 99 | }); 100 | -------------------------------------------------------------------------------- /hooks/AssignClientToGroupBasedOnPurchasedProduct_v2.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('EmailPreSend', 1, function($vars) 15 | { 16 | // Define group/product pairs. Instructions provided below 17 | // https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/README.md#client-to-group-based-on-purchased-productservice 18 | $groups['products']['1'] = array('1', '2', '3'); 19 | $groups['products']['2'] = array('4'); 20 | $groups['productaddons']['1'] = array('2'); 21 | $groups['configurableoption']['3'] = array('5' => true, '6' => array('7', '8', '10')); 22 | 23 | if (!$groups): return; endif; 24 | 25 | if (in_array($vars['messagename'], array('CodeGuard Welcome Email', 'Dedicated/VPS Server Welcome Email', 'Hosting Account Welcome Email', 'Marketgoo Welcome Email', 'Other Product/Service Welcome Email', 'Reseller Account Welcome Email', 'SHOUTcast Welcome Email', 'SiteLock VPN Welcome Email', 'SiteLock Welcome Email', 'SpamExperts Welcome Email', 'Weebly Welcome Email'))) 26 | { 27 | $orderedProducts = Capsule::table('tblhosting')->where('orderid', $vars['mergefields']['service_order_id'])->pluck('packageid'); 28 | $orderedProductAddons = Capsule::table('tblhostingaddons')->where('orderid', $vars['mergefields']['service_order_id'])->pluck('addonid'); 29 | 30 | if ($groups['configurableoption']) 31 | { 32 | foreach (Capsule::select(Capsule::raw('SELECT t1.relid, t1.configid, t1.optionid, t1.qty, t2.optiontype FROM tblhostingconfigoptions as t1 LEFT JOIN tblproductconfigoptions AS t2 ON t1.configid = t2.id LEFT JOIN tblproductconfigoptionssub AS t3 ON t1.optionid = t3.id WHERE t1.relid IN (\'' . implode('\',\'', array_keys($orderedProducts)) . '\')')) as $v) 33 | { 34 | $relid = $v->relid; 35 | $configid = $v->configid; 36 | 37 | if (in_array($v->optiontype, array('3', '4'))) 38 | { 39 | $value = ($v->qty ? true : false); 40 | } 41 | else 42 | { 43 | $value = $v->optionid; 44 | } 45 | 46 | unset($v); 47 | 48 | if ($value) 49 | { 50 | $orderedConfigurableOptions[$relid][$configid] = $value; 51 | } 52 | } 53 | } 54 | 55 | foreach ($groups['products'] as $group => $target) 56 | { 57 | if (array_intersect($orderedProducts, $target)) 58 | { 59 | Capsule::table('tblclients')->where('id', $vars['mergefields']['client_id'])->where('groupid', '0')->update(['groupid' => $group]); 60 | $groupName = Capsule::table('tblclientgroups')->where('id', $group)->pluck('groupname')[0]; 61 | $merge_fields['client_group_id'] = $group; 62 | $merge_fields['client_group_name'] = $groupName; 63 | $return = true; 64 | break; 65 | } 66 | } 67 | 68 | if ($return): return $merge_fields; endif; 69 | 70 | foreach ($groups['productaddons'] as $group => $target) 71 | { 72 | if (array_intersect($orderedProductAddons, $target)) 73 | { 74 | Capsule::table('tblclients')->where('id', $vars['mergefields']['client_id'])->where('groupid', '0')->update(['groupid' => $group]); 75 | $groupName = Capsule::table('tblclientgroups')->where('id', $group)->pluck('groupname')[0]; 76 | $merge_fields['client_group_id'] = $group; 77 | $merge_fields['client_group_name'] = $groupName; 78 | $return = true; 79 | break; 80 | } 81 | } 82 | 83 | if ($return): return $merge_fields; endif; 84 | 85 | foreach ($groups['configurableoption'] as $group => $configurableOptions) 86 | { 87 | foreach ($configurableOptions as $configID => $options) 88 | { 89 | if (is_array($options)) 90 | { 91 | foreach ($orderedConfigurableOptions as $target) 92 | { 93 | if (array_intersect($options, $target)) 94 | { 95 | Capsule::table('tblclients')->where('id', $vars['mergefields']['client_id'])->where('groupid', '0')->update(['groupid' => $group]); 96 | $groupName = Capsule::table('tblclientgroups')->where('id', $group)->pluck('groupname')[0]; 97 | $merge_fields['client_group_id'] = $group; 98 | $merge_fields['client_group_name'] = $groupName; 99 | $return = true; 100 | break; 101 | } 102 | } 103 | } 104 | else 105 | { 106 | foreach ($orderedConfigurableOptions as $target) 107 | { 108 | if (in_array($configID, $target)) 109 | { 110 | Capsule::table('tblclients')->where('id', $vars['mergefields']['client_id'])->where('groupid', '0')->update(['groupid' => $group]); 111 | $groupName = Capsule::table('tblclientgroups')->where('id', $group)->pluck('groupname')[0]; 112 | $merge_fields['client_group_id'] = $group; 113 | $merge_fields['client_group_name'] = $groupName; 114 | $return = true; 115 | break; 116 | } 117 | } 118 | } 119 | } 120 | } 121 | 122 | if ($return): return $merge_fields; endif; 123 | } 124 | }); 125 | -------------------------------------------------------------------------------- /hooks/AssignClientToGroupBasedOnRegisteredDomains.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('DailyCronJob', 1, function($vars) 15 | { 16 | // Define group/product pairs. Instructions provided below 17 | // https://github.com/Katamaze/WHMCS-Free-Action-Hooks#client-to-group-based-on-registered-domains 18 | $groups['1'] = '10'; 19 | $groups['2'] = '25'; 20 | $groups['3'] = '100'; 21 | 22 | $activeCustomers = true; 23 | $placeholderGroup = '1'; 24 | 25 | if (!$groups): return; endif; 26 | $defaultGroup = ($placeholderGroup ? $placeholderGroup : '0'); 27 | 28 | $filterStatus = ($activeCustomers ? ' AND t2.status = "Active"' : false); 29 | $filterGroup = ($placeholderGroup ? ' AND (t2.groupid = "' . $placeholderGroup . '" OR t2.groupid IN (\'' . implode('\', \'', array_keys($groups)) . '\'))' : false); 30 | 31 | foreach (Capsule::select(Capsule::raw('SELECT t1.userid, COUNT(t1.id) as total, t2.groupid FROM tbldomains AS t1 LEFT JOIN tblclients AS t2 ON t1.userid = t2.id WHERE t1.status IN ("Active", "Grace", "Redemption") ' . $filterStatus . ' GROUP BY t1.userid')) as $v) 32 | { 33 | $current[$v->userid] = $v; 34 | $users[$v->userid] = $defaultGroup; 35 | 36 | foreach ($groups as $gid => $total) 37 | { 38 | if ($v->total >= $total) 39 | { 40 | $users[$v->userid] = $gid; 41 | } 42 | } 43 | } 44 | 45 | foreach (Capsule::table('tblclientgroups')->select('id', 'groupname')->get() as $v) 46 | { 47 | $groupLabels[$v->id] = $v->groupname; 48 | } 49 | 50 | foreach ($users as $userID => $groupID) 51 | { 52 | if ($current[$userID]->groupid != $groupID) 53 | { 54 | logActivity('Client Group Modified - User ID: ' . $userID . ' now has ' . $current[$userID]->total . ' domain(s) - Moved from #' . $current[$userID]->groupid . ' ' . $groupLabels[$current[$userID]->groupid] . ' to #' . $groupID . ' ' . $groupLabels[$groupID]); 55 | } 56 | 57 | Capsule::table('tblclients')->where('id', $userID)->update(['groupid' => $groupID]); 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /hooks/AssignClientToGroupBasedOnRegistrationDate.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('DailyCronJob', 1, function($vars) 15 | { 16 | // Define group/product pairs. Instructions provided below 17 | // https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/README.md#client-to-group-based-on-registration-date 18 | $groups['1'] = '90'; 19 | $groups['2'] = '180'; 20 | $groups['3'] = '365'; 21 | 22 | $activeCustomers = true; 23 | $oldestPurchase = 10; 24 | $ignoreDomains = false; 25 | $ignoreProducts = array('1'); 26 | 27 | if (!$groups): return; endif; 28 | 29 | $filterStatus = ($activeCustomers ? ' AND status = "Active"' : false); 30 | 31 | if ($oldestPurchase) 32 | { 33 | $ignoreProducts = ($ignoreProducts ? ' AND t2.packageid NOT IN (\'' . implode('\',\'', $ignoreProducts) . '\')' : false); 34 | 35 | if ($ignoreDomains) 36 | { 37 | foreach (Capsule::select(Capsule::raw('SELECT t1.id FROM tblclients AS t1 LEFT JOIN tblhosting AS t2 ON t1.id = t2.userid WHERE DATEDIFF(CURDATE(), t2.regdate) >= "' . $oldestPurchase . '" '. $ignoreProducts .' GROUP BY t1.id')) as $v) 38 | { 39 | $filterPurchase[] = $v->id; 40 | } 41 | } 42 | else 43 | { 44 | foreach (Capsule::select(Capsule::raw('SELECT t1.id FROM tblclients AS t1 LEFT JOIN tblhosting AS t2 ON t1.id = t2.userid LEFT JOIN tbldomains AS t3 ON t1.id = t3.userid WHERE (DATEDIFF(CURDATE(), t2.regdate) >= "' . $oldestPurchase . '" '. $ignoreProducts .') OR DATEDIFF(CURDATE(), t3.registrationdate) >= "' . $oldestPurchase . '" GROUP BY t1.id')) as $v) 45 | { 46 | $filterPurchase[] = $v->id; 47 | } 48 | } 49 | 50 | if ($filterPurchase) 51 | { 52 | $filterPurchase = ' AND id IN (\'' . implode('\',\'', $filterPurchase) . '\')'; 53 | } 54 | } 55 | 56 | foreach ($groups as $groupID => $days) 57 | { 58 | Capsule::table('tblclients')->whereRaw('DATEDIFF(CURDATE(), datecreated) >= "' . $days . '"' . $filterStatus . $filterPurchase)->update(['groupid' => $groupID]); 59 | } 60 | }); 61 | -------------------------------------------------------------------------------- /hooks/AutoLoginToAnyPanelFromMyServices.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | // IMPORTANT! The hook requires changes to two template files. Read the following for instructions 13 | // https://github.com/Katamaze/WHMCS-Free-Action-Hooks/blob/master/README.md#cpanel--plesk-login-button-in-my-services 14 | 15 | use WHMCS\Database\Capsule; 16 | 17 | add_hook('ClientAreaPage', 1, function($vars) 18 | { 19 | if ($vars['filename'] == 'clientarea' AND $_GET['action'] == 'services' AND $_SESSION['uid']) 20 | { 21 | $productIDs = Capsule::select(Capsule::raw('SELECT t1.id, t2.type FROM tblhosting AS t1 LEFT JOIN tblservers AS t2 ON t1.server = t2.id WHERE t1.userid = ' . $_SESSION['uid'] . ' AND t1.server != "0" AND t1.domainstatus IN ("Active", "Suspended") AND t1.username != "" AND t2.type != ""')); 22 | 23 | foreach ($productIDs as $v) 24 | { 25 | $output['kt_autologin'][$v->id] = $v; 26 | } 27 | 28 | return $output; 29 | } 30 | elseif ($vars['filename'] == 'clientarea' AND $_GET['action'] == 'productdetails' AND $_GET['id'] AND $_GET['autologin']) 31 | { 32 | $product = Capsule::select(Capsule::raw('SELECT t2.type FROM tblhosting AS t1 LEFT JOIN tblservers AS t2 ON t1.server = t2.id WHERE t1.id = ' . $_GET['id'] . ' AND t1.server != "0" AND t1.domainstatus IN ("Active", "Suspended") AND t1.username IS NOT NULL AND t1.password IS NOT NULL AND t2.type IS NOT NULL LIMIT 1'))[0]; 33 | 34 | switch ($product->type) 35 | { 36 | case 'cpanel': header('Location: clientarea.php?action=productdetails&id=' . $_GET['id'] . '&dosinglesignon=1'); die(); break; 37 | } 38 | } 39 | }); 40 | 41 | add_hook('ClientAreaHeadOutput', 1, function($vars) 42 | { 43 | if ($vars['filename'] == 'clientarea' AND $_GET['action'] == 'productdetails' AND $_GET['id'] AND $_GET['autologin']) 44 | { 45 | return << 47 | body { 48 | visibility:hidden; 49 | } 50 | 51 | 57 | HTML; 58 | } 59 | }); 60 | -------------------------------------------------------------------------------- /hooks/AutoTerminateFreeTrialsAfterXMinutes.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('AfterCronJob', 1, function($vars) 15 | { 16 | $productIDs = array('1', '2'); // WHMCS Product IDs to terminate or suspend 17 | $terminateAfter = 30; // Terminate or suspend products after the given number of minutes (1440 = full day - 0 to disable) 18 | $performAction = 'Terminate'; // Terminate or Suspend 19 | $adminUsername = ''; // Optional for WHMCS 7.2 and later 20 | 21 | if ($productIDs AND $terminateAfter) 22 | { 23 | $date = new DateTime; 24 | $date = $date->format('Y-m-d H:i:s'); 25 | 26 | if ($performAction == 'Terminate') 27 | { 28 | $moduleAction = 'ModuleTerminate'; 29 | $domainStatus = 'Terminated'; 30 | } 31 | elseif ($performAction == 'Suspend') 32 | { 33 | $moduleAction = 'ModuleSuspend'; 34 | $domainStatus = 'Suspended'; 35 | } 36 | else 37 | { 38 | return; 39 | } 40 | 41 | $orderIDs = Capsule::table('tblorders')->whereRaw('NOW() <= date + INTERVAL 1 DAY')->pluck('date', 'id'); 42 | if (!$orderIDs): return; endif; 43 | $keys = array_keys($orderIDs); 44 | 45 | $hostingIDs = Capsule::table('tblhosting')->whereIn('orderid', $keys)->whereIn('packageid', $productIDs)->where('domainstatus', '!=', $domainStatus)->pluck('orderid', 'id'); 46 | if (!$hostingIDs): return; endif; 47 | 48 | $limit = new DateTime(); 49 | 50 | foreach ($hostingIDs as $k => $v) 51 | { 52 | $orderDate = new DateTime($orderIDs[$v]); 53 | $now = new DateTime('now'); 54 | $elapsed = $now->getTimestamp() - $orderDate->getTimestamp(); 55 | 56 | if ($elapsed >= $terminateAfter) 57 | { 58 | localAPI($moduleAction, array('serviceid' => $k), $adminUsername); 59 | $log[] = 'Service ID: ' . $k; 60 | } 61 | } 62 | 63 | if ($log) 64 | { 65 | logActivity('Free Trial ' . $performAction . ': ' . implode(', ', $log)); 66 | } 67 | } 68 | }); 69 | -------------------------------------------------------------------------------- /hooks/BanOrderExpiration.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Carbon; 13 | 14 | add_hook('AdminAreaHeadOutput', 1, function($vars) 15 | { 16 | if ($vars['filename'] == 'orders' AND $_GET['action'] == 'view' AND $_GET['id']) 17 | { 18 | $ban = Carbon::now(); 19 | $ban->modify('next year'); 20 | $year = $ban->format('Y'); 21 | $month = $ban->format('m'); 22 | $day = $ban->format('d'); 23 | 24 | return << 26 | $(document).ready(function(){ 27 | href = $('#contentarea table a[href^="configbannedips.php"]').attr('href'); 28 | href = href.replace(/(year=)[0-9]+/, 'year={$year}'); 29 | href = href.replace(/(month=)[0-9]+/, 'month={$month}'); 30 | href = href.replace(/(day=)[0-9]+/, 'day={$day}'); 31 | $('#contentarea table a[href^="configbannedips.php"]').attr('href', href); 32 | }) 33 | 34 | HTML; 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /hooks/BulkAutoRecalculateClientDomainsProducts.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | */ 12 | 13 | use WHMCS\Database\Capsule; 14 | 15 | add_hook('AdminAreaHeaderOutput', 1, function($vars) 16 | { 17 | if ($vars['filename'] == 'clientssummary' AND $_GET['userid'] AND in_array($_GET['kata'], array('bulkAutoRecalculateP', 'bulkAutoRecalculateD'))) 18 | { 19 | $adminUsername = ''; // Optional for WHMCS 7.2 and later 20 | 21 | if ($_GET['kata'] == 'bulkAutoRecalculateP') 22 | { 23 | foreach(Capsule::table('tblhosting')->where('userid', '=', $_GET['userid'])->pluck('id') as $v) 24 | { 25 | localAPI('UpdateClientProduct', array('serviceid' => $v, 'autorecalc' => true), $adminUsername); 26 | } 27 | 28 | header('Location: clientssummary.php?userid=' . $_GET['userid']); 29 | die(); 30 | } 31 | elseif ($_GET['kata'] == 'bulkAutoRecalculateD') 32 | { 33 | foreach (Capsule::table('tbldomains')->where('userid', '=', $_GET['userid'])->pluck('id') as $v) 34 | { 35 | localAPI('UpdateClientDomain', array('domainid' => $v, 'autorecalc' => true), $adminUsername); 36 | } 37 | 38 | header('Location: clientssummary.php?userid=' . $_GET['userid']); 39 | die(); 40 | } 41 | } 42 | 43 | return << 45 | $(document).ready(function(){ 46 | $('[href*="affiliates.php?action=edit&id="], [href*="clientssummary.php?userid="][href*="&activateaffiliate=true&token="]').closest('li').after(('
  • Bulk Auto Recalculate
  • ')); 47 | $('#kata_BulkAutoRecalculate').on('click', function(e){ 48 | e.preventDefault(); 49 | $('#modalAjaxTitle').html('Bulk Auto Recalculate'); 50 | $('#modalAjaxBody').html('

    Auto Recalculate Customer\'s Products/Services

    Recalculate Now »

    Auto Recalculate Customer\'s Domains

    Recalculate Now »

    '); 51 | $('#modalAjax .modal-submit').addClass('hidden'); 52 | $('#modalAjaxLoader').hide(); 53 | $('#modalAjax .modal-dialog').addClass('modal-lg'); 54 | $('#modalAjax').modal('show'); 55 | }) 56 | }) 57 | 58 | HTML; 59 | }); 60 | -------------------------------------------------------------------------------- /hooks/CancelOrderOnInvoiceCancelled.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('InvoiceCancelled', 1, function($vars) 15 | { 16 | $adminUsername = ''; // Optional for WHMCS 7.2 and later 17 | 18 | $orderID = Capsule::table('tblorders')->where('invoiceid', $vars['invoiceid'])->pluck('id')[0]; 19 | 20 | if ($orderID) 21 | { 22 | localAPI('CancelOrder', array('orderid' => $orderID), $adminUsername); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /hooks/ChangeDefaultSortingBackendTables.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | add_hook('AdminAreaPage', 1, function($vars) { 13 | 14 | if ($vars['filename'] == 'invoices' AND !$_COOKIE['WHMCSSD']) { 15 | 16 | setcookie('WHMCSSD', base64_encode(json_encode(array('invoices' => array('orderby' => 'date', 'sort' => 'DESC')))), time() + (86400 * 30), '/'); 17 | header('Location: invoices.php'); 18 | die(); 19 | } 20 | }); 21 | -------------------------------------------------------------------------------- /hooks/ChatstackDisableLoggedInAndAdmin.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | define('kt_disableAdminTracking', true); 15 | define('kt_disableForLoggedIn', true); 16 | 17 | add_hook('ClientAreaHeadOutput', 1, function($vars) 18 | { 19 | if (kt_disableAdminTracking AND !$vars['adminMasqueradingAsClient'] AND !$vars['adminLoggedIn']) 20 | { 21 | $host = ($_SERVER['HTTP_HOST'] ? $_SERVER['HTTP_HOST'] : parse_url($vars['systemurl'])['host']); 22 | 23 | return << 25 | 38 | 39 | HTML; 40 | } 41 | }); 42 | 43 | add_hook('ClientAreaFooterOutput', 1, function($vars) 44 | { 45 | if (kt_disableForLoggedIn AND $vars['loggedin']) 46 | { 47 | return << 49 | Chatstack.ready(function () { 50 | $("#chatstack-launcher-frame").remove(); 51 | }); 52 | 53 | HTML; 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /hooks/ClientGroupColorInTicketView.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('AdminAreaHeaderOutput', 1, function($vars) 15 | { 16 | foreach (Capsule::select(Capsule::raw('SELECT t1.id, t2.groupcolour AS color FROM tblclients AS t1 LEFT JOIN tblclientgroups AS t2 ON t1.groupid = t2.id WHERE groupid != "0"')) as $v) 17 | { 18 | $users[$v->id] = $v->color; 19 | } 20 | 21 | $users = json_encode($users); 22 | 23 | return << 25 | $(document).on('ready', function() { 26 | var data = {$users}; 27 | 28 | $("table#sortabletbl2 tr:has(td), table#sortabletbl1 tr:has(td)").each(function () { 29 | var href = $(this).find('td:nth-child(5) a[href^="clientssummary.php?userid="]'); 30 | if (typeof href !== undefined) 31 | { 32 | var params = $(href).attr('href'); 33 | 34 | if (params) { 35 | var params = params.split('='); 36 | var id = params[params.length - 1]; 37 | 38 | if (typeof data[id] !== 'undefined') { 39 | $(this).find('a[href^="clientssummary.php?userid="]').css('background-color', data[id]); 40 | } 41 | } 42 | } 43 | }); 44 | }); 45 | 46 | HTML; 47 | }); 48 | -------------------------------------------------------------------------------- /hooks/ConditionalClientCustomFieldsBasedOnSelectedCountry.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | 16 | use WHMCS\Database\Capsule; 17 | 18 | define('kt_custom_fields_by_country', [ 19 | 20 | 'IT' => [ 1, 2 ], // When `IT` (Italy) is selected only custom fields ID `1` and `2` are displayed. Other fields are automatically hidden 21 | 'IL' => [ 3, 4 ] // When `IL` (Israel) is selected only custom fields ID `3` and `4` are displayed. Other fields are automatically hidden 22 | ]); 23 | 24 | // Array is empty. Nothing to do. Exiting... 25 | if (!kt_custom_fields_by_country) { 26 | 27 | return; 28 | } 29 | 30 | add_hook('ClientAreaHeadOutput', 1, function($vars) { 31 | 32 | // The hook triggers only on `regsiter.php` and `cart.php?a=checkout` where users register on WHMCS 33 | if ($vars['filename'] != 'register' AND ($vars['filename'] != 'cart' AND $_GET['a'] != 'checkout')) { 34 | 35 | return; 36 | } 37 | 38 | $output = ' 39 | 40 | 99 | 100 | '; 101 | 102 | return $output; 103 | 104 | }); 105 | -------------------------------------------------------------------------------- /hooks/ConditionalSupportDepartments.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('ClientAreaPage', 1, function($vars) 15 | { 16 | // Define department/product pairs. Instructions provided below 17 | // https://github.com/Katamaze/WHMCS-Free-Scripts/blob/master/README.md#conditional-support-departments 18 | $department['1'] = array('1', '46', '47'); // The access to support department #1 (the ['1'] between square brackets) is restricted to users with product ID #45, #45 and #47 19 | $department['2'] = array('12', '13', '14'); 20 | 21 | if ($vars['filename'] != 'submitticket'): return; endif; 22 | if (!$department): return; endif; 23 | $departmentIDs = array_keys($department); 24 | 25 | foreach (Capsule::select(Capsule::raw('SELECT packageid FROM tblhosting WHERE userid = "' . $vars['clientsdetails']['userid'] . '" AND domainstatus NOT IN ("Pending", "Suspended", "Terminated", "Cancelled", "Fraud") GROUP BY packageid')) as $v) 26 | { 27 | $packageIDs[] = $v->packageid; 28 | } 29 | 30 | foreach ($vars['departments'] as $k => $v) 31 | { 32 | if (!in_array($v['id'], $departmentIDs)) 33 | { 34 | $overrideDepartments[$k] = $vars['departments'][$k]; 35 | $allowedDepartments[] = $v['id']; 36 | } 37 | else 38 | { 39 | if (!array_intersect($department[$v['id']], $packageIDs)) 40 | { 41 | $overrideDepartments[$k] = $vars['departments'][$k]; 42 | $allowedDepartments[] = $v['id']; 43 | } 44 | } 45 | } 46 | 47 | if ($_GET['deptid'] AND !in_array($_GET['deptid'], $allowedDepartments)): header('Location: submitticket.php'); die(); endif; 48 | 49 | return array('departments' => $overrideDepartments); 50 | }); 51 | -------------------------------------------------------------------------------- /hooks/ContactsEmailConfirmation.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | 14 | use WHMCS\Database\Capsule; 15 | 16 | add_hook('ContactAdd', 1, function($vars) 17 | { 18 | if (!Capsule::schema()->hasTable('kt_contacts')) 19 | { 20 | Capsule::select(Capsule::raw('CREATE TABLE `kt_contacts` (`id` int(11) NOT NULL, `userid` int(11) NOT NULL, `data` text COLLATE utf8_unicode_ci NOT NULL, `created_at` date NOT NULL DEFAULT current_timestamp()) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;')); 21 | Capsule::select(Capsule::raw('ALTER TABLE `kt_contacts` ADD PRIMARY KEY (`id`);')); 22 | } 23 | 24 | $userID = $vars['userid']; 25 | $contactID = $vars['contactid']; 26 | unset($vars['userid'], $vars['contactid']); 27 | 28 | Capsule::table('kt_contacts')->insert(array('id' => $contactID, 'userid' => $userID, 'data' => json_encode($vars))); 29 | Capsule::table('tblcontacts')->where('id', $contactID)->delete(); 30 | }); 31 | 32 | add_hook('AdminAreaHeadOutput', 1, function($vars) 33 | { 34 | if ($vars['filename'] == 'clientscontacts' AND $_GET['userid']) 35 | { 36 | $pendingContacts = Capsule::table('kt_contacts')->where('userid', $_GET['userid'])->get(); 37 | 38 | if ($pendingContacts) 39 | { 40 | $total = count($pendingContacts); 41 | $total = ($total == '1' ? $total . ' contact' : $total . ' contacts'); 42 | 43 | foreach ($pendingContacts as $contact) 44 | { 45 | $data = json_decode($contact->data); 46 | $email = $data->email; 47 | unset($data->email); 48 | 49 | foreach ($data as $k => $v) 50 | { 51 | if ($v) 52 | { 53 | $temp .= $k . ': ' . $v . '
    '; 54 | } 55 | } 56 | 57 | $table .= '' . $email . '' . $temp . '' . $contact->created_at . ''; 58 | } 59 | 60 | return << 62 | $(document).ready(function(){ 63 | $('select[name="contactid"]').after((' {$total} awaiting confirmation')); 64 | 65 | $("#expandContacts").click(function() { 66 | $("#modalAjax .modal-header").html('Contacts with Unconfirmed Emails'); 67 | $("#modalAjax .modal-body").html('{$table}
    EmailDataDate
    '); 68 | $('#modalAjax .loader').hide(); 69 | $('#modalAjax .modal-submit').html('Mark as Confirmed'); 70 | $("#modalAjax").modal('show'); 71 | }); 72 | }) 73 | 74 | HTML; 75 | } 76 | } 77 | }); 78 | -------------------------------------------------------------------------------- /hooks/CouponCodeInEmailTemplate.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('EmailPreSend', 1, function($vars) 15 | { 16 | if (in_array($vars['messagename'], array('Invoice Payment Confirmation'))) 17 | { 18 | $promotions = Capsule::select(Capsule::raw('SELECT description FROM tblinvoiceitems WHERE invoiceid = "' . $vars['relid'] . '" AND type = "PromoHosting"')); 19 | 20 | foreach ($promotions as $v) 21 | { 22 | $merge_fields['assigned_promos'][] = $v->description; 23 | } 24 | 25 | return $merge_fields; 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /hooks/DailyCronJonOnDemand.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('AdminAreaHeaderOutput', 1, function($vars) 15 | { 16 | if ($_GET['simulatecron']) { 17 | 18 | $SystemURL = Capsule::table('tblconfiguration')->where('setting', 'SystemURL')->first(['value'])->value; 19 | Capsule::table('tblconfiguration')->where('setting', 'lastDailyCronInvocationTime')->update(['value' => '']); 20 | Capsule::table('tblconfiguration')->where('setting', 'DailyCronExecutionHour')->update(['value' => date('H')]); 21 | 22 | $ch = curl_init(); 23 | curl_setopt($ch, CURLOPT_URL, $SystemURL . 'crons/cron.php'); 24 | curl_setopt($ch, CURLOPT_HEADER, 0); 25 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 26 | curl_exec($ch); 27 | curl_close($ch); 28 | } 29 | 30 | return << 32 | .katademo1 { color:#fff !important; background-color:#eaae53 !important;} 33 | 34 | 56 | HTML; 57 | }); 58 | -------------------------------------------------------------------------------- /hooks/DisableFeedbackRequestsForUnansweredTickets.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('EmailPreSend', 1, function($vars) 15 | { 16 | if ($vars['messagename'] == 'Support Ticket Feedback Request') 17 | { 18 | if (!Capsule::table('tblticketreplies')->where('tid', $vars['relid'])->where('admin', '!=', '')->pluck('id')[0]) 19 | { 20 | return array('abortsend' => true); 21 | } 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /hooks/ExemptExistingClientsFromAffiliateCommissions.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('AffiliateCommission', 1, function($vars) 15 | { 16 | $numberOfDays = '10'; 17 | 18 | if ($numberOfDays) 19 | { 20 | if (!Capsule::select(Capsule::raw('SELECT t1.id FROM tblhosting AS t1 LEFT JOIN tblclients AS t2 ON t1.userid = t2.id WHERE t1.id = "' . $vars['serviceId'] . '" AND DATEDIFF(CURDATE(), t2.datecreated) <= "' . $numberOfDays . '"'))) 21 | { 22 | $return['skipCommission'] = true; 23 | return $return; 24 | } 25 | } 26 | }); 27 | -------------------------------------------------------------------------------- /hooks/ForcePaymentGatewayDependingOnInvoiceBalance.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | define('kt_gateway', 'banktransfer'); // Force this payment gateway when invoice balance is >= $limit. Use System Name (eg. banktransfer, paypal) 15 | define('kt_limit', '0'); // Specifiy the limit in WHMCS Default Currency. The hook automatically handles currency conversion (0 to disable) 16 | define('kt_countries', array()); // Optionally define countries where you want to apply this hook. Use ISO 3166-1 alpha-2 country codes (eg. IT, FR, US) 17 | define('kt_europe', false); // Set true to use the hook on EU-based customers. This option can be used together with $countries 18 | 19 | add_hook('ClientAreaPage', 100, function($vars) { 20 | 21 | if ($vars['filename'] == 'viewinvoice' AND $_GET['id']) { 22 | 23 | if (kt_countries AND !in_array($vars['clientsdetails']['countrycode'], kt_countries)): return; endif; 24 | if (kt_europe AND !in_array($vars['clientsdetails']['countrycode'], array('AT', 'BE', 'BE', 'BG', 'HR', 'CY', 'CZ', 'DK', 'EE', 'FK', 'FI', 'FR', 'DE', 'GR', 'HU', 'IE', 'IM', 'IT', 'LV', 'LT', 'LU', 'MT', 'MC', 'NL', 'PL', 'PT', 'RO', 'SK', 'SI', 'ES', 'SE', 'GB'))): return; endif; 25 | 26 | if (kt_gateway AND kt_limit) { 27 | 28 | $currencyRate = Capsule::table('tblcurrencies')->where('id', '=', $vars['clientsdetails']['currency'])->where('default', '!=', '1')->pluck('rate')[0]; 29 | $adminUnlock = Capsule::table('tblinvoices')->where('id', '=', $_GET['id'])->where('notes', 'like', '%Payment Method Unlocked by Administratror%')->pluck('notes')[0]; 30 | } 31 | 32 | if ($adminUnlock) { 33 | 34 | $vars['notes'] = str_replace('Payment Method Unlocked by Administratror', '', $vars['notes']); 35 | 36 | if (!$vars['notes']) { 37 | 38 | return array('notes' => false); 39 | } 40 | else { 41 | 42 | return array('notes' => $vars['notes']); 43 | } 44 | } 45 | 46 | if ($currencyRate) { 47 | 48 | $balance = number_format($vars['balance']->toNumeric() / $currencyRate, 2, '.', ''); 49 | } 50 | else { 51 | 52 | $balance = $vars['balance']->toNumeric(); 53 | } 54 | 55 | if ($balance >= kt_limit AND !$adminUnlock) { 56 | 57 | if ($vars['paymentmodule'] == kt_gateway) { 58 | 59 | return array('allowchangegateway' => false); 60 | } 61 | else { 62 | 63 | Capsule::table('tblinvoices')->where('id', $_GET['id'])->update(['paymentmethod' => kt_gateway]); 64 | header('Location: viewinvoice.php?id=' . $_GET['id']); 65 | die(); 66 | } 67 | } 68 | } 69 | }); 70 | 71 | add_hook('ClientAreaHeadOutput', 1, function($vars) { 72 | 73 | if ($vars['filename'] == 'clientarea' AND $_GET['action'] == 'addfunds' AND kt_limit AND kt_gateway) { 74 | 75 | $limit = kt_limit; 76 | $gateway = kt_gateway; 77 | 78 | return << 80 | $(document).ready(function() { 81 | 82 | var selectOptions = $('select[name="paymentmethod"]').html(); 83 | 84 | $('input[name="amount"]').keyup(function() { 85 | 86 | var amount = $('input[name="amount"]').val(); 87 | 88 | if(amount >= {$limit}) { 89 | 90 | $('select[name="paymentmethod"] option').each(function() { 91 | 92 | if ($(this).val() == '{$gateway}') { 93 | 94 | $(this).prop('selected', true); 95 | } 96 | else { 97 | 98 | $(this).remove(); 99 | } 100 | }); 101 | 102 | } else { 103 | 104 | $('select[name="paymentmethod"]').html(selectOptions); 105 | } 106 | }); 107 | }); 108 | 109 | HTML; 110 | } 111 | }); 112 | 113 | add_hook('InvoiceChangeGateway', 1, function($vars) { 114 | 115 | if ($_SESSION['adminid']) { 116 | 117 | $currentNotes = Capsule::table('tblinvoices')->where('id', $vars['invoiceid'])->pluck('notes')[0]; 118 | $currentNotes = str_replace('Payment Method Unlocked by Administratror', '', $currentNotes); 119 | Capsule::table('tblinvoices')->where('id', $vars['invoiceid'])->update(['notes' => 'Payment Method Unlocked by Administratror' . $currentNotes]); 120 | } 121 | }); 122 | -------------------------------------------------------------------------------- /hooks/GenerateUUID.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | use Ramsey\Uuid\Uuid; 14 | 15 | add_hook('ClientAreaPage', 1, function($vars) 16 | { 17 | $clients = Capsule::table('tblclients')->where('uuid', '')->pluck('id'); 18 | 19 | foreach ($clients as $v) 20 | { 21 | Capsule::table('tblclients')->where('id', $v)->update(['uuid' => Uuid::uuid4()]); 22 | } 23 | }); 24 | -------------------------------------------------------------------------------- /hooks/HideGoogleInvisibleReCAPTCHA.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | add_hook('ClientAreaHeadOutput', 1, function($vars) 13 | { 14 | return << 16 | .grecaptcha-badge { opacity:0 } 17 | 18 | HTML; 19 | }); 20 | -------------------------------------------------------------------------------- /hooks/IfClientsGroupThisThenThat.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | define('kt_groups', [ 15 | 16 | // Rules for client group id `1` (visit `configclientgroups.php`) 17 | 1 => [ 18 | 19 | 'tax_exempt' => true, // `true` = enable tax exempt | `false` = disable tax exempt | `null` = don't cange. Changes to tax exempt status only apply to new invoices. Existing ones are left unchanged 20 | 'invoice_header' => 'Ferrari S.p.A.' . PHP_EOL . 'Via Emilia Est 1163' . PHP_EOL . '41121 Modena - Italia', // Replaces the `Pay to` section of invoices with this one. Set `false` to leave default details. IMPORTANT! This does't affect `invoicepdf.tpl` file (the PDF version of invoice). This file can't be accessed via hooks. You'll need to put an if/else statement yourself 21 | 'allowed_payment_gateways' => [ // Array of payment gateways that clients from this group are allowed to use. Leave empty for no restriction 22 | 23 | 'paypalcheckout', // The first gateway in the array is always used as default when it comes to replacing restricted ones from open invoices (not in `Paid`, `Collections`, `Refund`, `Payment Pending` status) 24 | 'banktransfer' 25 | ] 26 | ], 27 | // Rules for client group id `2` (visit `configclientgroups.php`) 28 | 2 => [ 29 | 30 | 'tax_exempt' => false, 31 | 'invoice_header' => 'Juventus S.p.A.' . PHP_EOL . 'Via Druento 175' . PHP_EOL . '10151 Torino - Italia', 32 | 'allowed_payment_gateways' => [ 33 | 34 | 'banktransfer' 35 | ] 36 | ] 37 | ]); 38 | 39 | // If the above array is empty there's no need to go on with the script. It ends here 40 | if (!kt_groups) { 41 | 42 | return; 43 | } 44 | 45 | function kt_LoadCompanySettings($client_id) 46 | { 47 | // Retreive client group id 48 | $client_group_id = Capsule::table('tblclients')->where('id', $client_id)->whereIn('id', array_keys(kt_groups))->pluck('groupid')[0]; 49 | 50 | // Client has no group or there is no custom rule defined for his group. We can exit 51 | if (!$client_group_id OR !isset(kt_groups[$client_group_id])) { 52 | 53 | return; 54 | } 55 | 56 | $settings = kt_groups[$client_group_id]; 57 | 58 | // We have restrictions to payment gateways to take care of 59 | if ($settings['allowed_payment_gateways']) { 60 | 61 | // List of all payment gateways of WHMCS 62 | $payment_gateways = Capsule::table('tblpaymentgateways')->where('setting', 'name')->pluck('gateway')->toArray(); 63 | 64 | // Calculate the difference between allowed gateways and available ones. In essence the array contains the list of gatways that should be restricted 65 | $restricted_gateways = array_diff($payment_gateways, $settings['allowed_payment_gateways']); 66 | 67 | // We found at least one restricted gateway 68 | if ($restricted_gateways) { 69 | 70 | $settings['restricted_gateways'] = $restricted_gateways; 71 | } 72 | } 73 | 74 | // Array that contains all custom settings defined in kt_groups for the selected group/client 75 | return [ 76 | 77 | 'client_id' => $client_id, 78 | 'settings' => $settings 79 | ]; 80 | } 81 | 82 | function kt_UpdateClient($data = null) 83 | { 84 | // No data. There's nothing to do on the client in question. Exiting... 85 | if (!$data) { 86 | 87 | return; 88 | } 89 | 90 | // Updating tax exempt status of client depending on company's settings 91 | if ($data['settings']['tax_exempt'] === true) { 92 | 93 | $tax_exempt = 1; 94 | } 95 | elseif ($data['settings']['tax_exempt'] === false) { 96 | 97 | $tax_exempt = 0; 98 | } 99 | 100 | // We appy change only if `tax_exempt` was set in `kt_group` as `true` or `false` 101 | if (isset($tax_exempt)) { 102 | 103 | Capsule::table('tblclients')->where('id', $data['client_id'])->update(['taxexempt' => $tax_exempt]); 104 | } 105 | 106 | // Removing restricted gateway(s) from open invoices (not in `Paid`, `Collections`, `Refunded`, `Payment Pending` status) of current client. Restricted gatewaty get replaced with the first gateway defined in `kt_groups.allowed_payment_gateways` 107 | if ($data['settings']['restricted_gateways']) { 108 | 109 | Capsule::table('tblinvoices')->where('userid', $data['client_id'])->whereNotIn('status', [ 'Paid', 'Collections', 'Refunded', 'Payment Pending' ])->whereIn('paymentmethod', $data['settings']['restricted_gateways'])->update(['paymentmethod' => $data['settings']['allowed_payment_gateways'][0]]); 110 | } 111 | } 112 | 113 | // Apply `kt_groups` settings as a client is being added to WHMCS 114 | add_hook('ClientAdd', 1, function($vars) { 115 | 116 | $data = kt_LoadCompanySettings($vars['client_id']); 117 | kt_UpdateClient($data); 118 | }); 119 | 120 | // Apply `kt_groups` settings when a client is edited through the client area, admin area or API 121 | add_hook('ClientEdit', 1, function($vars) { 122 | 123 | $data = kt_LoadCompanySettings($vars['userid']); 124 | kt_UpdateClient($data); 125 | }); 126 | 127 | // Apply `kt_groups` settings when a client is viewing an invoice 128 | add_hook('ClientAreaPageViewInvoice', 1, function($vars) { 129 | 130 | $data = kt_LoadCompanySettings($vars['userid']); 131 | 132 | // No changes to apply to `viewinvoice.php` 133 | if (!isset($data['settings']['restricted_gateways']) AND !$data['settings']['invoice_header']) { 134 | 135 | return; 136 | } 137 | 138 | // At least one payment gateway is restricted to the client 139 | if ($data['settings']['restricted_gateways']) { 140 | 141 | // Parsing `$vars['gatewaydropdown']` as HTML (the variable contains the HTML of `Payment method` dropdown accessible from `viewinvoice.php`) 142 | $dom = new DOMDocument(); 143 | $dom->loadHTML($vars['gatewaydropdown']); 144 | 145 | // I feed DOM to xPath in order to access `` tags of the `` as array 146 | $xpath = new DomXPath($dom); 147 | 148 | // Prepare array to store xPath select conditions. I need this to tell xPath that for example I don't want `paypal` and `banktransfer` in the dropdown 149 | $xpath_conditions = []; 150 | 151 | // Looping every restricted payment gateway 152 | foreach ($data['settings']['restricted_gateways'] as $v) { 153 | 154 | $xpath_conditions[] = '@value="' . $v . '"'; 155 | } 156 | 157 | // Imploding conditions by `" or "` as xPath is expecting 158 | $xpath_conditions = implode($xpath_conditions, ' or '); 159 | 160 | // Looping every `` of restricted payment gateways 161 | foreach($xpath->query('//select/option[(' . $xpath_conditions . ')]') as $node) { 162 | 163 | // Removing the restricted payment gateway from dropdown 164 | $node->parentNode->removeChild($node); 165 | } 166 | 167 | // Overriding default WHMCS dropdown with mine 168 | $vars['gatewaydropdown'] = $dom->saveXml(); 169 | } 170 | 171 | // `Pay to` needs to be changed 172 | if ($data['settings']['invoice_header']) { 173 | 174 | $vars['payto'] = nl2br($data['settings']['invoice_header']); 175 | } 176 | 177 | return [ 178 | 179 | 'gatewaydropdown' => $vars['gatewaydropdown'], 180 | 'payto' => $vars['payto'] 181 | ]; 182 | }); 183 | -------------------------------------------------------------------------------- /hooks/InactiveIsTheNewActive.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | * USE AT YOUR OWN RISK 12 | * 13 | * In the middle of the COVID-19 crisis WHMCS increased prices up to 3154% 14 | * Some companies will start paying 15.599 $ per year instead of 479 $ 15 | * 16 | * I stopped supporting WHMCS a long time ago due to the following reasons: 17 | * https://katamaze.com/blog/41/whmcs-cons 18 | * 19 | * I'm no longer in the mood to help this company with my money, efforts and skills 20 | * I stopped wasting my time with their untested releses full of bugs and features no one asked for 21 | * I also stopped adding new features to my modules and I no longer partecipate to whmcs.community 22 | * 23 | * They don't deserve it 24 | * 25 | * That being said, the following script can surely help to figure out what to do 26 | * I prefer not to describe what it does but it's not rocket science 27 | * This is something I coded in less than 10 minutes 28 | * 29 | * I can think of 2 more ways to achieve the same goal in more stylish ways (all legitimate) 30 | * Anyway I moved to another market leaving WHMCS so I have no time to dedicate to this project 31 | */ 32 | 33 | add_hook('AdminAreaHeadOutput', 1, function($vars) { 34 | 35 | if ($vars['filename'] == 'clients' OR in_array($_GET['rp'], array('/admin/services', '/admin/addons', '/admin/domains'))) { 36 | 37 | $autoPost = << 49 | $(document).on('ready', function() { 50 | 51 | if ($('input#intelliSearchHideInactiveSwitch').is(':checked')) { 52 | 53 | $('#intelliSearchHideInactiveSwitch').click(); 54 | $('#intelligentSearchResults').css('display', 'none'); 55 | } 56 | 57 | {$autoPost} 58 | }) 59 | 60 | HTML; 61 | }); 62 | -------------------------------------------------------------------------------- /hooks/KnowledgebaseAuthor.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('AdminAreaHeadOutput', 1, function($vars) 15 | { 16 | if ($vars['filename'] == 'supportkb' AND $_GET['action'] == 'edit' AND $_GET['id']) 17 | { 18 | if (!Capsule::schema()->hasColumn('tblknowledgebase', 'kt_author')) 19 | { 20 | Capsule::select(Capsule::raw('ALTER TABLE `tblknowledgebase` ADD `kt_author` INT NULL AFTER `language`')); 21 | } 22 | 23 | $adminID = Capsule::table('tblknowledgebase')->where('id', $_GET['id'])->pluck('kt_author')[0]; 24 | 25 | if (!$adminID) 26 | { 27 | $adminID = $_SESSION['adminid']; 28 | Capsule::table('tblknowledgebase')->where('id', $_GET['id'])->update(['kt_author' => $adminID]); 29 | } 30 | 31 | $adminUsername = Capsule::table('tbladmins')->where('id', $adminID)->pluck('username')[0]; 32 | 33 | return << 35 | $(document).ready(function(){ 36 | $('table[class="form"] > tbody tr:first').after(('Author {$adminUsername}')); 37 | }) 38 | 39 | HTML; 40 | } 41 | }); 42 | -------------------------------------------------------------------------------- /hooks/KnowledgebaseLastUpdatedDate.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('ClientAreaPageKnowledgebase', 1, function($vars) 15 | { 16 | if ($vars['kbarticle']['id']) 17 | { 18 | $LastUpdated = Capsule::table('tblactivitylog')->where('description', 'like', 'Modified Knowledgebase Article ID: %' . $vars['kbarticle']['id'])->orderby('id', 'desc')->first(['date'])->date; 19 | $output['kbarticle'] = $vars['kbarticle']; 20 | $output['kbarticle']['lastupdated']['date'] = date('Y-m-d', strtotime($LastUpdated)); 21 | $output['kbarticle']['lastupdated']['time'] = date('H:i:s', strtotime($LastUpdated)); 22 | 23 | return $output; 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /hooks/LoginAsClientPreserveLanguage.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | add_hook('AdminAreaHeaderOutput', 1, function($vars) 13 | { 14 | if ($vars['filename'] == 'clientssummary' AND $_GET['userid']) 15 | { 16 | return << 18 | $(document).on('ready', function(){ 19 | 20 | href = $("#summary-login-as-client").attr('href'); 21 | 22 | if (typeof href !== "undefined") { 23 | 24 | $("#summary-login-as-client").attr('href', href.replace(/&?language=\w+/, '')); 25 | } 26 | 27 | href = $("#summary-login-as-owner").attr('href'); 28 | 29 | if (typeof href !== "undefined") { 30 | 31 | $("#summary-login-as-owner").attr('href', href.replace(/&?language=\w+/, '')); 32 | 33 | href = $("#summary-login-as-owner-new-window").attr('href'); 34 | $("#summary-login-as-owner-new-window").attr('href', href.replace(/&?language=\w+/, '')); 35 | } 36 | }); 37 | 38 | HTML; 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /hooks/NewClientsAsAffiliates.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | add_hook('ClientAreaRegister', 1, function($vars) 13 | { 14 | $adminUsername = 'ADMIN_USERNAME'; // Optional for WHMCS 7.2 and later 15 | $results = localAPI('AffiliateActivate', array('userid' => $vars['userid']), $adminUsername); 16 | }); 17 | -------------------------------------------------------------------------------- /hooks/NoteInTheOrderAbortAutoProvisioning.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('PreModuleCreate', 1, function($vars) 15 | { 16 | $Data = Capsule::select(Capsule::raw('SELECT t1.notes FROM tblorders AS t1 LEFT JOIN tblhosting AS t2 ON t1.id = t2.orderid WHERE t2.id = "' . $vars['params']['serviceid'] . '" AND t2.orderid != "0"')); 17 | 18 | if ($Data[0]->notes) 19 | { 20 | return array('abortcmd' => true); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /hooks/NotifyFradulentOrders.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('FraudOrder', 1, function($vars) 15 | { 16 | $admins = Capsule::table('tbladmins')->where('disabled', '=', '0')->pluck('username'); 17 | 18 | foreach ($admins as $username) 19 | { 20 | localAPI('SendAdminEmail', array('type' => 'system', 'customsubject' => 'Fraud Order Detected', 'custommessage' => 'Order #' . $vars['orderid'] . ' detected as Fraudulent'), $username); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /hooks/OneOffProductsDomainRequireProduct.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | define('kt_onetimeProducts', array()); // Array of product IDs to treat as "one-off" (customer is not allowed to order the same product multiple times 15 | define('kt_onetimeProductGroups', array()); // Same as above but for product group IDs. All producs inside such groups are treated as one-off 16 | define('kt_firstTimerTollerance', true); // Product-based restrictions are disabled for new customers placing their first order with you 17 | define('kt_notRepeatable', true); // If a customer already has a one-off product, he can't purchase further one-offs ($firstTimerTollerance is ignored) 18 | define('kt_domainRequiresProduct', false); // Domain purchase is allowed only if any of the following conditions is met: a) Customer has an existing product/service (`Pending` and `Terminated` don't count) b) Customer is purchasing a domain and a product/service 19 | define('kt_onClientRegister', false); // Ordering one-off products is possible only for clients who registered within the last X number of days. Leave false to disable 20 | define('kt_promptRemoval', 'modal'); // Choose one of the following options: "bootstrap-alert", "modal", "js-alert" (works on Six template. Change jQuery selectors accordingly for custom templates) 21 | define('kt_textDisallowed', 'The Product/Service can be purchased only once.'); // Don't forget to "\" escape 22 | define('kt_textRequireProduct', 'Domain purchase require an active Product/Service.'); // Don't forget to "\" escape 23 | 24 | add_hook('ClientAreaHeadOutput', 1, function($vars) 25 | { 26 | if ($_SESSION['cart']['products'] AND (kt_onetimeProductGroups OR kt_onetimeProducts)) 27 | { 28 | $disallowedPids = Capsule::table('tblproducts')->whereIn('gid', kt_onetimeProductGroups)->orWhereIn('id', kt_onetimeProducts)->pluck('id'); 29 | $productsInCart = array_column($_SESSION['cart']['products'], 'pid'); 30 | 31 | if ($_SESSION['uid'] AND kt_onClientRegister) 32 | { 33 | $isNewCustomer = Capsule::table('tblclients')->where('id', '=', $_SESSION['uid'])->whereDate('datecreated', '>=', Carbon::now()->subDays(kt_onClientRegister))->pluck('id'); 34 | 35 | if (!$isNewCustomer) 36 | { 37 | foreach ($_SESSION['cart']['products'] as $k => $v) 38 | { 39 | if (in_array($v['pid'], $disallowedPids)) 40 | { 41 | $removedFromCart = true; 42 | unset($_SESSION['cart']['products'][$k]); 43 | } 44 | } 45 | } 46 | } 47 | 48 | if ($_SESSION['uid']) 49 | { 50 | $userProducts = Capsule::table('tblhosting')->where('userid', '=', $_SESSION['uid'])->WhereIn('packageid', $disallowedPids)->groupBy('packageid')->pluck('packageid'); 51 | } 52 | 53 | if (kt_notRepeatable) 54 | { 55 | $groupByProducts = array_count_values($productsInCart); 56 | $groupByProductsKeys = array_keys($groupByProducts); 57 | $i = 1; 58 | 59 | foreach ($_SESSION['cart']['products'] as $k => $v) 60 | { 61 | if (in_array($v['pid'], $groupByProductsKeys) AND in_array($v['pid'], $disallowedPids)) 62 | { 63 | if ($i > 1) 64 | { 65 | $removedFromCart = true; 66 | unset($_SESSION['cart']['products'][count($_SESSION['cart']['products'])-1]); 67 | } 68 | 69 | $i++; 70 | } 71 | } 72 | } 73 | elseif (!kt_firstTimerTollerance) 74 | { 75 | foreach ($productTotals as $k => $v) 76 | { 77 | if ($v > 1) 78 | { 79 | $removedFromCart = true; 80 | unset($_SESSION['cart']['products'][count($_SESSION['cart']['products'])-1]); 81 | } 82 | } 83 | } 84 | 85 | foreach ($_SESSION['cart']['products'] as $k => $v) 86 | { 87 | if (in_array($v['pid'], $userProducts)) 88 | { 89 | $removedFromCart = true; 90 | unset($_SESSION['cart']['products'][$k]); 91 | } 92 | 93 | $productTotals[$v['pid']]++; 94 | } 95 | 96 | if ($removedFromCart) 97 | { 98 | header('Location: cart.php?a=view&disallowed=1'); 99 | die(); 100 | } 101 | } 102 | 103 | if ($_SESSION['cart']['domains'] AND kt_domainRequiresProduct) 104 | { 105 | $userHasProduct = Capsule::table('tblhosting')->where('userid', '=', $_SESSION['uid'])->whereNotIn('domainstatus', array('Pending', 'Terminated'))->pluck('id'); 106 | 107 | if (!$userHasProduct AND !$_SESSION['cart']['products']) 108 | { 109 | unset($_SESSION['cart']['domains']); 110 | header('Location: cart.php?a=view&requireProduct=1'); 111 | die(); 112 | } 113 | } 114 | }); 115 | 116 | add_hook('ClientAreaHeadOutput', 1, function($vars) 117 | { 118 | if ($vars['filename'] == 'cart' AND $_GET['a'] == 'view') 119 | { 120 | if ($_GET['disallowed']): $text = kt_textDisallowed; 121 | elseif ($_GET['requireProduct']): $text = kt_textRequireProduct; endif; 122 | 123 | if (kt_promptRemoval == 'bootstrap-alert') 124 | { 125 | $code = <<{$text}'); 127 | HTML; 128 | } 129 | elseif (kt_promptRemoval == 'modal') 130 | { 131 | $code = <<
    {$text}
    '); 134 | $('#modalAjax .loader').hide(); 135 | $('#modalAjax .modal-submit').hide(); 136 | $("#modalAjax").modal('show'); 137 | HTML; 138 | } 139 | elseif (kt_promptRemoval == 'js-alert') 140 | { 141 | $code = << 150 | $(document).ready(function() { 151 | {$code} 152 | }); 153 | 154 | HTML; 155 | } 156 | } 157 | }); 158 | 159 | add_hook('AdminAreaHeadOutput', 1, function($vars) 160 | { 161 | if ($vars['filename'] == 'configproducts') 162 | { 163 | $objPrododucts = json_encode(kt_onetimeProducts); 164 | $objGroups = json_encode(kt_onetimeProductGroups); 165 | 166 | return << 168 | $(document).ready(function() { 169 | $.each({$objPrododucts}, function(key, value) { 170 | $('#tableBackground > table > tbody > tr').find("a[href$='?action=edit&id=" + value + "']").closest('tr').find('td').css('background-color', '#d2eed0'); 171 | $('#tableBackground > table > tbody > tr').find("a[href$='?action=edit&id=" + value + "']").closest('tr').find('td:first').append(' '); 172 | }); 173 | 174 | $.each({$objGroups}, function(key, value) { 175 | $('#tableBackground > table > tbody > tr').find("a[href$='?action=editgroup&ids=" + value + "']").closest('tr').find('td').css('background-color', '#d2eed0'); 176 | $('#tableBackground > table > tbody > tr').find("a[href$='?action=editgroup&ids=" + value + "']").closest('tr').find('td:first > div.prodGroup').append(' '); 177 | }); 178 | }); 179 | 180 | HTML; 181 | } 182 | }); 183 | -------------------------------------------------------------------------------- /hooks/PreventAccessToBackendDuringMaintenance.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | define('kt_maintenance_mode_allowed_admins', [ ]); // Array of Admin Ids that are allowed to access WHMCS backend when maintenance mode is enabled 15 | define('kt_maintenance_mode_allowed_admin_roles', [ ]); // Array of Admin Role Ids that are allowed to access WHMCS backend when maintenance mode is enabled 16 | 17 | // If the above arrays are empty there's no need to go on with the script. It ends here 18 | if (!kt_maintenance_mode_allowed_admin_roles AND !kt_maintenance_mode_allowed_admins) { 19 | 20 | return; 21 | } 22 | 23 | add_hook('AdminAreaPage', 1, function($vars) { 24 | 25 | // Detect if Mainenance Mode is enabled or disabled 26 | $maintenance_mode = Capsule::table('tblconfiguration')->where('setting', 'MaintenanceMode')->pluck('value')[0]; 27 | 28 | // Maintenance Mode is disabled. Exiting... 29 | if ($maintenance_mode != 'on') { 30 | 31 | return; 32 | } 33 | 34 | // `kt_maintenance_mode_allowed_admins` is set. Verify if the currently logged admin can access backend during maintenance 35 | if (kt_maintenance_mode_allowed_admins) { 36 | 37 | // Not allowed. Forcing logout... 38 | if (!in_array($_SESSION['adminid'], kt_maintenance_mode_allowed_admins)) { 39 | 40 | header('Location: logout.php?'); 41 | die(); 42 | } 43 | } 44 | 45 | // `kt_maintenance_mode_allowed_admin_roles` is set. Verify if the currently logged admin group can access backend during maintenance 46 | if (kt_maintenance_mode_allowed_admin_roles) { 47 | 48 | $admin_role_id = Capsule::table('tbladmins')->where('id', $_SESSION['adminid'])->pluck('roleid')[0]; 49 | 50 | // Not allowed. Forcing logout... 51 | if (!in_array($admin_role_id, kt_maintenance_mode_allowed_admin_roles)) { 52 | 53 | header('Location: logout.php'); 54 | die(); 55 | } 56 | } 57 | }); 58 | -------------------------------------------------------------------------------- /hooks/PreventChangesToClientCustomFields.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('CustomFieldSave', 1, function($vars) 15 | { 16 | $ReadOnlyFields = array('1', '2'); // IDs of Custom Fields that cannot be edited 17 | $DisallowAdmin = false; // true = Even Administrators are not allowed to edit | false = Administrators can freely update Custom Fields 18 | 19 | /* Do not edit below */ 20 | $IsAdmin = (basename($_SERVER['PHP_SELF']) == 'clientsprofile.php' ? true : false); 21 | $IsNewClient = (in_array(basename($_SERVER['PHP_SELF']), array('register.php', 'cart.php')) ? true : false); 22 | 23 | if (in_array($vars['fieldid'], $ReadOnlyFields) AND (($IsAdmin AND $DisallowAdmin) OR (!$IsAdmin)) AND !$IsNewClient) 24 | { 25 | return array('value' => Capsule::table('tblcustomfieldsvalues')->where(['fieldid' => $vars['fieldid'], 'relid' => $vars['relid']])->first(['value'])->value); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /hooks/PreventEmailSendingBasedOnClientGroup.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('EmailPreSend', 1, function($vars) 15 | { 16 | $disallowedGroupIDs = array('1', '2'); // Array of Client Group ID to block 17 | $emailTemplates = array('Automated Password Reset', 'Password Reset Validation', 'Password Reset Confirmation'); // Email Templates to block (General Messages) 18 | 19 | if (in_array($vars['messagename'], $emailTemplates)) 20 | { 21 | if (!Capsule::select(Capsule::raw('SELECT id FROM tblclients WHERE id = "' . $vars['relid'] . '" AND groupid IN (\'' . implode('\',\'', $disallowedGroupIDs) . '\') LIMIT 1'))) 22 | { 23 | $output['abortsend'] = true; 24 | return $output; 25 | } 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /hooks/PreventSearchEngineIndexing.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | add_hook('ClientAreaHeadOutput', 1, function($vars) 13 | { 14 | return << 16 | HTML; 17 | }); 18 | -------------------------------------------------------------------------------- /hooks/PromotionsArrayInEmailTemplates.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('EmailPreSend', 1, function($vars) 15 | { 16 | $emailTemplates = array('Invoice Payment Confirmation'); // Array of Email Templates in which you want to include Promotions array 17 | 18 | if (in_array($vars['messagename'], $emailTemplates)) 19 | { 20 | $promotions = json_decode(json_encode(Capsule::table('tblpromotions')->get()), true); 21 | 22 | foreach ($promotions as $k => $v) 23 | { 24 | if ($v['expirationdate'] != '0000-00-00' AND date('Y-m-d') >= $v['expirationdate']) 25 | { 26 | unset($promotions[$k]); 27 | continue; 28 | } 29 | 30 | if ($v['maxuses'] > '0' AND $v['uses'] == $v['maxuses']) 31 | { 32 | unset($promotions[$k]); 33 | continue; 34 | } 35 | } 36 | 37 | return array('promotions' => $promotions); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /hooks/QuoteToInvoiceNoRedirect.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use Illuminate\Database\Capsule\Manager as Capsule; 13 | 14 | add_hook('InvoiceCreation', 1, function($vars) 15 | { 16 | $quoteID = Capsule::table('tblinvoices')->where('id', $vars['invoiceid'])->where('notes', 'like', 'Re Quote #%')->first(['notes']); 17 | header('Location: quotes.php?action=manage&id=' . explode('#', $quoteID->notes)[1]); 18 | die(); 19 | }); 20 | -------------------------------------------------------------------------------- /hooks/RelatedServiceInInfoTicketSidebar.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | use WHMCS\View\Menu\Item as MenuItem; 14 | 15 | add_hook('ClientAreaPrimarySidebar', 1, function (MenuItem $primarySidebar) 16 | { 17 | if (!is_null($primarySidebar->getChild('Ticket Information'))) 18 | { 19 | $relatedService = Capsule::table('tbltickets')->where('tid', '=', $_GET['tid'])->pluck('service')[0]; 20 | if (!$relatedService): return; endif; 21 | 22 | $serviceType = substr($relatedService, 0, 1); 23 | $relatedService = substr($relatedService, 1); 24 | 25 | if ($serviceType == 'D') 26 | { 27 | $url = 'clientarea.php?action=domaindetails&id=' . $relatedService; 28 | $target = Capsule::table('tbldomains')->where('id', '=', $relatedService)->pluck('domain')[0]; 29 | $icon = ''; 30 | $label = $icon . ' ' . $target . ''; 31 | } 32 | elseif ($serviceType == 'S') 33 | { 34 | $url = 'clientarea.php?action=productdetails&id=' . $relatedService; 35 | $target = Capsule::table('tblhosting')->leftJoin('tblproducts', 'tblhosting.packageid', '=', 'tblproducts.id')->where('tblhosting.id', '=', $relatedService)->pluck('tblproducts.name')[0]; 36 | $icon = ''; 37 | $label = $icon . ' ' . $target . ''; 38 | } 39 | 40 | $primarySidebar->getChild('Ticket Information') 41 | ->addChild('Related Service') 42 | ->setClass('ticket-details-children') 43 | ->setLabel('Related Service
    ' . $label) 44 | ->setOrder(20); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /hooks/RemoveIPAddressFromViewTicketClientArea.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | add_hook('ClientAreaPage', 1, function($vars) { 13 | 14 | if ($vars['templatefile'] == 'viewticket') { 15 | 16 | $output = []; 17 | 18 | foreach ($vars['ascreplies'] as $index => $replies) { 19 | 20 | foreach ($replies as $k => $v) { 21 | 22 | if ($k == 'ipaddress') { 23 | 24 | $v = false; 25 | } 26 | 27 | $output['ascreplies'][$index][$k] = $v; 28 | } 29 | } 30 | 31 | foreach ($vars['descreplies'] as $index => $replies) { 32 | 33 | foreach ($replies as $k => $v) { 34 | 35 | if ($k == 'ipaddress') { 36 | 37 | $v = false; 38 | } 39 | 40 | $output['descreplies'][$index][$k] = $v; 41 | } 42 | } 43 | 44 | return $output; 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /hooks/RemovePortalHomeBreadcrumb.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | add_hook('ClientAreaPage', 1, function($vars) 13 | { 14 | unset($vars['breadcrumb'][0]); 15 | return array('breadcrumb' => $vars['breadcrumb']); 16 | }); 17 | -------------------------------------------------------------------------------- /hooks/RenameAddonModuleLabel.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | add_hook('AdminAreaHeaderOutput', 1, function($vars) 13 | { 14 | return << 16 | $(document).ready(function(){ 17 | $("a[id='Menu-Addons-Mercury']").text('CMS'); 18 | $("a[id='Menu-Addons-Commission Manager']").text('Affiliates'); 19 | $("a[id='Menu-Addons-Billing Extension']").text('Accounting'); 20 | }); 21 | 22 | HTML; 23 | }); 24 | -------------------------------------------------------------------------------- /hooks/RestrictDomainBillingCyclesBasedOnTLD.php: -------------------------------------------------------------------------------- 1 | 10 | * @license MIT License 11 | * @tested WHMCS 7.10.2 12 | */ 13 | 14 | use WHMCS\Database\Capsule; 15 | 16 | add_hook('ClientAreaPage', 1, function($vars) 17 | { 18 | if (($vars['filename'] == 'cart' AND $_GET['a'] == 'view') OR $vars['templatefile'] == 'domain-renewals') 19 | { 20 | $restrictedTLDs = array('.com'); // Specify TLD for which you want to restrict billing cycles. TLD must start with a dot "." 21 | $restrictedPeriods = array('2', '3', '4', '5', '6', '7', '8', '9', '10'); // Any value from 1 to 10 (1 year, 2 years, 3 years... 10 years) 22 | 23 | if (!$restrictedTLDs OR !$restrictedPeriods): return; endif; 24 | $key = ($vars['templatefile'] == 'domain-renewals' ? 'renewalsData' : 'domains'); 25 | 26 | foreach ($vars[$key] as $k => $v) 27 | { 28 | $tld = Capsule::select(Capsule::raw('SELECT extension FROM tbldomainpricing WHERE "' . $v['domain'] . '" LIKE CONCAT("%", extension) ORDER BY LENGTH(extension) DESC LIMIT 1'))[0]->extension; 29 | 30 | if (in_array($tld, $restrictedTLDs)) 31 | { 32 | if ($key == 'domains') 33 | { 34 | foreach ($restrictedPeriods as $i) 35 | { 36 | unset($vars[$key][$k]['pricing'][$i]); 37 | } 38 | } 39 | else 40 | { 41 | foreach ($vars[$key][$k]['renewalOptions'] as $k2 => $v2) 42 | { 43 | if (in_array($vars[$key][$k]['renewalOptions'][$k2]['period'], $restrictedPeriods)) 44 | { 45 | unset($vars[$key][$k]['renewalOptions'][$k2]); 46 | } 47 | } 48 | } 49 | } 50 | } 51 | 52 | return array($key => $vars[$key]); 53 | } 54 | }); 55 | -------------------------------------------------------------------------------- /hooks/RestrictPaymentGatewaysBasedOnClientGroup.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | define('kt_groups', [ 15 | 16 | // Rules for client group id `1` (visit `configclientgroups.php`) 17 | 1 => [ 18 | 19 | 'allowed_payment_gateways' => [ // Array of payment gateways that clients from this group are allowed to use. Leave empty for no restriction 20 | 21 | 'paypalcheckout', // The first element is always used as replacement for open invoices (not in `Paid`, `Collections`, `Refund`, `Payment Pending` status) that are configured to use restricted gateways 22 | 'banktransfer' 23 | ] 24 | ], 25 | // Rules for to client group id `2` (visit `configclientgroups.php`) 26 | 2 => [ 27 | 28 | 'allowed_payment_gateways' => [ 29 | 30 | 'banktransfer' 31 | ] 32 | ] 33 | ]); 34 | 35 | // If the above array is empty there's no need to go on with the script. It ends here 36 | if (!kt_groups) { 37 | 38 | return; 39 | } 40 | 41 | function kt_LoadCompanySettings($client_id) 42 | { 43 | // Retreive client group id 44 | $client_group_id = Capsule::table('tblclients')->where('id', $client_id)->whereIn('id', array_keys(kt_groups))->pluck('groupid')[0]; 45 | 46 | // Client has no group or there is no custom rule defined for his group. We can exit 47 | if (!$client_group_id OR !isset(kt_groups[$client_group_id])) { 48 | 49 | return; 50 | } 51 | 52 | $settings = kt_groups[$client_group_id]; 53 | 54 | // We have restrictions to payment gateways to take care of 55 | if ($settings['allowed_payment_gateways']) { 56 | 57 | // List of all payment gateways of WHMCS 58 | $payment_gateways = Capsule::table('tblpaymentgateways')->where('setting', 'name')->pluck('gateway')->toArray(); 59 | 60 | // Calculate the difference between allowed gateways and available ones. In essence the array contains the list of gatways that should be restricted 61 | $restricted_gateways = array_diff($payment_gateways, $settings['allowed_payment_gateways']); 62 | 63 | // We found at least one restricted gateway 64 | if ($restricted_gateways) { 65 | 66 | $settings['restricted_gateways'] = $restricted_gateways; 67 | } 68 | } 69 | 70 | // Array that contains all custom settings defined in kt_groups for the selected group/client 71 | return [ 72 | 73 | 'client_id' => $client_id, 74 | 'settings' => $settings 75 | ]; 76 | } 77 | 78 | function kt_UpdateClient($data = null) 79 | { 80 | // No data. There's nothing to do on the client in question. Exiting... 81 | if (!$data) { 82 | 83 | return; 84 | } 85 | 86 | // Removing restricted gateway(s) from open invoices (not in `Paid`, `Collections`, `Refunded`, `Payment Pending` status) of current client. Restricted gatewaty get replaced with the first gateway defined in `kt_groups.allowed_payment_gateways` 87 | if ($data['settings']['restricted_gateways']) { 88 | 89 | Capsule::table('tblinvoices')->where('userid', $data['client_id'])->whereNotIn('status', [ 'Paid', 'Collections', 'Refunded', 'Payment Pending' ])->whereIn('paymentmethod', $data['settings']['restricted_gateways'])->update(['paymentmethod' => $data['settings']['allowed_payment_gateways'][0]]); 90 | } 91 | } 92 | 93 | // Apply `kt_groups` settings as a client is being added to WHMCS 94 | add_hook('ClientAdd', 1, function($vars) { 95 | 96 | $data = kt_LoadCompanySettings($vars['client_id']); 97 | kt_UpdateClient($data); 98 | }); 99 | 100 | // Apply `kt_groups` settings when a client is edited through the client area, admin area or API 101 | add_hook('ClientEdit', 1, function($vars) { 102 | 103 | $data = kt_LoadCompanySettings($vars['userid']); 104 | kt_UpdateClient($data); 105 | }); 106 | 107 | // Apply `kt_groups` settings when a client is viewing an invoice 108 | add_hook('ClientAreaPageViewInvoice', 1, function($vars) { 109 | 110 | $data = kt_LoadCompanySettings($vars['userid']); 111 | 112 | // No changes to apply to `viewinvoice.php` 113 | if (!isset($data['settings']['restricted_gateways'])) { 114 | 115 | return; 116 | } 117 | 118 | // At least one payment gateway is restricted to the client 119 | if ($data['settings']['restricted_gateways']) { 120 | 121 | // Parsing `$vars['gatewaydropdown']` as HTML (the variable contains the HTML of `Payment method` dropdown accessible from `viewinvoice.php`) 122 | $dom = new DOMDocument(); 123 | $dom->loadHTML($vars['gatewaydropdown']); 124 | 125 | // I feed DOM to xPath in order to access `` tags of the `` as array 126 | $xpath = new DomXPath($dom); 127 | 128 | // Prepare array to store xPath select conditions. I need this to tell xPath that for example I don't want `paypal` and `banktransfer` in the dropdown 129 | $xpath_conditions = []; 130 | 131 | // Looping every restricted payment gateway 132 | foreach ($data['settings']['restricted_gateways'] as $v) { 133 | 134 | $xpath_conditions[] = '@value="' . $v . '"'; 135 | } 136 | 137 | // Imploding conditions by `" or "` as xPath is expecting 138 | $xpath_conditions = implode($xpath_conditions, ' or '); 139 | 140 | // Looping every `` of restricted payment gateways 141 | foreach($xpath->query('//select/option[(' . $xpath_conditions . ')]') as $node) { 142 | 143 | // Removing the restricted payment gateway from dropdown 144 | $node->parentNode->removeChild($node); 145 | } 146 | 147 | // Overriding default WHMCS dropdown with mine 148 | $vars['gatewaydropdown'] = $dom->saveXml(); 149 | } 150 | 151 | return [ 152 | 153 | 'gatewaydropdown' => $vars['gatewaydropdown'] 154 | ]; 155 | }); 156 | -------------------------------------------------------------------------------- /hooks/SendEmailAndAddReplyOnTicketStatusChange.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('TicketStatusChange', 1, function($vars) { 15 | 16 | $adminUsername = 'admin'; // The reply will be added by this Admin user. Set false to open the ticket using your own customer 17 | $ticketDetails = Capsule::table('tbltickets')->where('id', $vars['ticketid'])->first(['userid', 'tid', 'title']); 18 | 19 | // Email notification 20 | $EmailData = array( 21 | 'id' => $ticketDetails->userid, 22 | 'customtype' => 'general', 23 | 'customsubject' => $ticketDetails->title. ' Changed to ' . strtoupper($vars['status']) . ' [Ticket ID #' . $ticketDetails->tid . ']', 24 | 'custommessage' => 'Your ticket status has been changed to ' .$vars['status'] 25 | ); 26 | 27 | localAPI('SendEmail', $EmailData); 28 | 29 | // Ticket reply 30 | $TicketData = array( 31 | 'ticketid' => $vars['ticketid'], 32 | 'message' => $ticketDetails->title. ' Changed to ' . strtoupper($vars['status']) . ' [Ticket ID #' . $ticketDetails->tid . ']', 33 | 'clientid' => $userID, 34 | 'adminusername' => $adminUsername, 35 | ); 36 | 37 | localAPI('AddTicketReply', $TicketData); 38 | }); 39 | -------------------------------------------------------------------------------- /hooks/StrongerPasswordGeneratorForAutoProvisioning_v1.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('OverrideModuleUsernameGeneration', 1, function($vars) 15 | { 16 | $password = substr(str_shuffle('abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!@#$%^&*()-=+?'), 0, $length = '10'); 17 | Capsule::table('tblhosting')->where('id', $vars['params']['serviceid'])->update(['password' => Encrypt($password)]); 18 | }); 19 | -------------------------------------------------------------------------------- /hooks/StrongerPasswordGeneratorForAutoProvisioning_v2.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('OverrideModuleUsernameGeneration', 1, function($vars) 15 | { 16 | $chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'; 17 | $specialChars = '!@#$%^&*?'; // Plesk does not consider (, ), -, = and + as special characters 18 | $password = substr(str_shuffle($chars), 0, $length = '9'); 19 | $randomPos = rand(0, strlen($password) - 1); 20 | $randomSpecialChar = $specialChars[rand(0,strlen($specialChars)-1)]; 21 | $password = substr($password, 0, $randomPos) . $randomSpecialChar . substr($password, $randomPos); 22 | 23 | Capsule::table('tblhosting')->where('id', $vars['params']['serviceid'])->update(['password' => Encrypt($password)]); 24 | }); 25 | -------------------------------------------------------------------------------- /hooks/StrongerPasswordGeneratorForAutoProvisioning_v3.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('OverrideModuleUsernameGeneration', 1, function($vars) 15 | { 16 | $length['digit'] = '3'; // Number of digits in password 17 | $length['lower'] = '4'; // Number of UNIQUE lowercase characters in password 18 | $length['upper'] = '4'; // Number of UNIQUE uppercase characters in password 19 | $length['special'] = '2'; // Number of special characters in password 20 | 21 | // The same character cannot be used more than once (case sensitive) 22 | if ($length['lower'] + $length['upper'] == '26') 23 | { 24 | $length['lower'] = '13'; 25 | $length['upper'] = '13'; 26 | } 27 | 28 | $digits = '0123456789'; 29 | $chars = 'abcdefghijklmnopqrstuvwxyz'; 30 | $special = '!@#$%^&*?'; // Plesk does not consider (, ), -, = and + as special characters 31 | $digits = substr(str_shuffle($digits), 0, $length['digit']); 32 | $lower = substr(str_shuffle($chars), 0, $length['lower']); 33 | $upper = substr(str_shuffle(strtoupper(str_replace(str_split($lower), '', $chars) )), 0, $length['lower']); 34 | $special = substr(str_shuffle($special), 0, $length['special']); 35 | $password = str_shuffle($digits . $lower . $upper . $special); 36 | 37 | Capsule::table('tblhosting')->where('id', $vars['params']['serviceid'])->update(['password' => Encrypt($password)]); 38 | }); 39 | -------------------------------------------------------------------------------- /hooks/TicketFeedbackEscalationRule.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | */ 12 | 13 | use WHMCS\Database\Capsule; 14 | 15 | add_hook('AfterCronJob', 1, function($vars) 16 | { 17 | $cronFrequency = '5'; // In minutes. Normally you should configure it to run every 5 minutes 18 | $adminUsername = ''; // Optional for WHMCS 7.2 and later 19 | 20 | if ($cronFrequency) 21 | { 22 | $ticketLog = Capsule::select(Capsule::raw('SELECT t1.tid, t2.userid, SUBSTRING_INDEX(SUBSTRING_INDEX(t1.action, " \"", -1), "\" ", 1) as escalationrule FROM tblticketlog AS t1 LEFT JOIN tbltickets AS t2 ON t1.tid = t2.id WHERE t1.date >= DATE_FORMAT(DATE_SUB(NOW(), INTERVAL 5 MINUTE), "%Y-%m-%d %H:%i:00") AND t1.action LIKE ("Escalation Rule \"%%\" applied") GROUP BY t1.tid')); 23 | $ticketLog = json_decode(json_encode($ticketLog), true); 24 | $escalationRules = Capsule::table('tblticketescalations')->where('newstatus', 'Closed')->pluck('name'); 25 | 26 | foreach ($ticketLog as $v) 27 | { 28 | $key = array_search($v['escalationrule'], $escalationRules); 29 | 30 | if ($key) 31 | { 32 | $preventFeebackBouncing = Capsule::table('tblactivitylog')->where('userid', $v['userid'])->where('description', 'LIKE', 'Support Ticket Feedback Request Sent %')->pluck('id'); 33 | 34 | if (!$preventFeebackBouncing) 35 | { 36 | logActivity('Support Ticket Feedback Request Sent (Escalation Rule: ' . $escalationRules[$key] . ') - Ticket ID: ' . $v['tid'] . ' - User ID: ' . $v['userid'], $v['userid']); 37 | localAPI('SendEmail', array('messagename' => 'Support Ticket Feedback Request', 'id' => $v['tid']), $adminUsername); 38 | } 39 | } 40 | } 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /hooks/UpdateAdminLinksWhenCustomAdminPathChanges.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | use WHMCS\Config\Setting; 14 | 15 | define('KtConfigurationCustomAdminPath', $config->customadminpath); 16 | 17 | add_hook('AdminAreaPage', 1, function($vars) { 18 | 19 | // Custom Admin Path not set. Nothing to do 20 | if (!KtConfigurationCustomAdminPath) { 21 | 22 | return; 23 | } 24 | 25 | $stored_custom_admin_path = Setting::getValue('KtStoredCustomAdminPath'); 26 | 27 | // Storing current Custom Admin Path in tblconfigurations table to detect when it changes 28 | if (empty($stored_custom_admin_path)) { 29 | 30 | Setting::setValue('KtStoredCustomAdminPath', KtConfigurationCustomAdminPath); 31 | return; 32 | } 33 | 34 | // The stored and live version of Custom Admin Path (the one in configuration.php) are the same. Nothing to do 35 | if (KtConfigurationCustomAdminPath == $stored_custom_admin_path) { 36 | 37 | return; 38 | } 39 | 40 | // If we're here it means Custom Admin Path has been updated so we need to perform a couple of replacements 41 | $system_url = Setting::getValue('SystemURL'); 42 | $find = $system_url . '/' . Setting::getValue('KtStoredCustomAdminPath'); 43 | $replace = $system_url . '/' . KtConfigurationCustomAdminPath; 44 | 45 | Capsule::table('tbladmins')->update([ 'notes' => Capsule::raw('REPLACE(notes, "' . $find . '", "' . $replace . '")') ]); 46 | Capsule::table('tblnotes')->update([ 'note' => Capsule::raw('REPLACE(note, "' . $find . '", "' . $replace . '")') ]); 47 | Capsule::table('tblticketnotes')->update([ 'message' => Capsule::raw('REPLACE(`message`, "' . $find . '", "' . $replace . '")') ]); 48 | Capsule::table('tbltodolist')->update([ 'description' => Capsule::raw('REPLACE(`description`, "' . $find . '", "' . $replace . '")') ]); 49 | 50 | // Uncomment if you have Project Management on your system 51 | //Capsule::table('mod_projectmessages')->update([ 'message' => Capsule::raw('REPLACE(`message`, "' . $find . '", "' . $replace . '")') ]); 52 | //Capsule::table('mod_projecttasks')->update([ 'task' => Capsule::raw('REPLACE(`task`, "' . $find . '", "' . $replace . '")'), 'notes' => Capsule::raw('REPLACE(`notes`, "' . $find . '", "' . $replace . '")') ]); 53 | 54 | Setting::setValue('KtStoredCustomAdminPath', KtConfigurationCustomAdminPath); 55 | }); 56 | -------------------------------------------------------------------------------- /hooks/noDatesInInvoiceItemsDescription.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | 12 | use WHMCS\Database\Capsule; 13 | 14 | add_hook('InvoiceCreationPreEmail', 1, function($vars) { 15 | 16 | $items = Capsule::table('tblinvoiceitems')->select('id', 'description')->where('invoiceid', '=', $vars['invoiceid'])->get(); 17 | 18 | if (!$items) { 19 | 20 | return; 21 | } 22 | 23 | $dateFormat = Capsule::table('tblconfiguration')->select('value')->where('setting', '=', 'DateFormat')->first(); 24 | 25 | if (in_array($dateFormat->value, [ 'DD/MM/YYYY', 'DD.MM.YYYY', 'DD-MM-YYYY' ])) { 26 | 27 | $regex = '/[" "][(](((0[1-9]|[12][0-9]|3[01])[- \/.](0[13578]|1[02])|(0[1-9]|[12][0-9]|30)[- \/.](0[469]|11)|(0[1-9]|1\d|2[0-8])[- \/.]02)[- \/.]\d{4}|29[- \/.]02[- \/.](\d{2}(0[48]|[2468][048]|[13579][26])|([02468][048]|[1359][26])00))[\w -]*[-]*[\w -](((0[1-9]|[12][0-9]|3[01])[- \/.](0[13578]|1[02])|(0[1-9]|[12][0-9]|30)[- \/.](0[469]|11)|(0[1-9]|1\d|2[0-8])[- \/.]02)[- \/.]\d{4}|29[- \/.]02[- \/.](\d{2}(0[48]|[2468][048]|[13579][26])|([02468][048]|[1359][26])00))[)]/'; 28 | } 29 | elseif ($dateFormat->value == 'MM/DD/YYYY') { 30 | 31 | $regex = '/[" "][(](((0[13578]|1[02])|(0[1-9]|[12][0-9]|30)[-\/.](0[469]|11)|(0[1-9]|1\d|2[0-8])[-\/.]02)[-\/.](0[1-9]|[12][0-9]|3[01])[-\/.]\d{4}|29[-\/.]02[-\/.](\d{2}(0[48]|[2468][048]|[13579][26])|([02468][048]|[1359][26])00))[\w -]*[-]*[\w -](((0[13578]|1[02])|(0[1-9]|[12][0-9]|30)[-\/.](0[469]|11)|(0[1-9]|1\d|2[0-8])[-\/.]02)[-\/.](0[1-9]|[12][0-9]|3[01])[-\/.]\d{4}|29[-\/.]02[-\/.](\d{2}(0[48]|[2468][048]|[13579][26])|([02468][048]|[1359][26])00))[)]/'; 32 | } 33 | elseif (in_array($dateFormat->value, [ 'YYYY/MM/DD', 'YYYY-MM-DD' ])) { 34 | 35 | $regex = '/[" "][(](\d{4}|29[-\/.]02[-\/.](\d{2}(0[48]|[2468][048]|[13579][26])|([02468][048]|[1359][26])00))[-\/.]((0[13578]|1[02])|(0[1-9]|[12][0-9]|30)[-\/.](0[469]|11)|(0[1-9]|1\d|2[0-8])[-\/.]02)[-\/.](0[1-9]|[12][0-9]|3[01])[\w -]*[-]*[\w -](\d{4}|29[-\/.]02[-\/.](\d{2}(0[48]|[2468][048]|[13579][26])|([02468][048]|[1359][26])00))[-\/.]((0[13578]|1[02])|(0[1-9]|[12][0-9]|30)[-\/.](0[469]|11)|(0[1-9]|1\d|2[0-8])[-\/.]02)[-\/.](0[1-9]|[12][0-9]|3[01])[)]/'; 36 | } 37 | 38 | foreach ($items AS $v) { 39 | 40 | $v->description = preg_replace($regex, '', $v->description); 41 | Capsule::table('tblinvoiceitems')->where('id', '=', $v->id)->update([ 'description' => $v->description ]); 42 | } 43 | }); 44 | -------------------------------------------------------------------------------- /modules/addons/PleskChecker/PleskChecker.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | */ 12 | 13 | if (!defined("WHMCS")) 14 | die("This file cannot be accessed directly"); 15 | 16 | function PleskChecker_config() 17 | { 18 | $configarray = array( 19 | "name" => "Plesk Checker", 20 | "description" => 'Check for missing integrations between WHMCS Hosting Accounts and Plesk Servers', 21 | "version" => "1.0.0", 22 | "author" => "", 23 | "fields" => array()); 24 | 25 | return $configarray; 26 | } 27 | 28 | function PleskChecker_activate() 29 | { 30 | 31 | } 32 | 33 | function PleskChecker_deactivate() 34 | { 35 | 36 | } 37 | 38 | function PleskChecker_upgrade($vars) 39 | { 40 | 41 | } 42 | 43 | function PleskChecker_output($vars) 44 | { 45 | $smarty = new Smarty(); 46 | $smarty->caching = false; 47 | $smarty->compile_dir = $GLOBALS['templates_compiledir']; 48 | $smarty->setTemplateDir(array(0 => '../modules/addons/PleskChecker/templates/Admin')); 49 | 50 | require_once('core/Katamaze/Checker.php'); 51 | $checker = new Checker(); 52 | 53 | $smarty->assign('checker', $checker->Plesk()); 54 | $smarty->display(dirname(__FILE__) . '/templates/Admin/Main.tpl'); 55 | } 56 | -------------------------------------------------------------------------------- /modules/addons/PleskChecker/core/Katamaze/Checker.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | */ 12 | 13 | use WHMCS\Database\Capsule; 14 | 15 | class Checker 16 | { 17 | function Plesk() 18 | { 19 | foreach (Capsule::table('tblservers')->where('type', 'plesk')->where('disabled', '0')->where('hostname', '!=', '')->where('username', '!=', '')->where('password', '!=', '')->get(['id', 'ipaddress', 'hostname', 'username', 'password']) as $v) 20 | { 21 | $v->hostname = ($v->hostname ? $v->hostname : $v->ipaddress); 22 | unset($v->ipaddress); 23 | $v->password = Decrypt($v->password); 24 | $output['servers'][$v->id] = (array) $v; 25 | } 26 | 27 | if (!$output['servers']): return array('error' => 'No Plesk servers Found. Please, check your Servers.'); endif; 28 | $servers = array_column($output['servers'], 'id'); 29 | 30 | $externalID = Capsule::table('mod_pleskaccounts')->pluck('panelexternalid', 'userid'); 31 | 32 | foreach (Capsule::table('tblhosting')->whereIn('domainstatus', ['Active', 'Suspended', 'Completed'])->whereIn('server', $servers)->get(['id', 'username', 'domain', 'server', 'userid']) as $v) 33 | { 34 | if (!$v->domain) 35 | { 36 | $output['error']['noDomain'][$v->id] = array('id' => $v->id, 'userid' => $v->userid, 'server' => $v->server); 37 | } 38 | elseif (!$v->username) 39 | { 40 | $output['error']['noUsername'][$v->id] = array('id' => $v->id, 'userid' => $v->userid, 'domain' => $v->domain, 'server' => $v->server); 41 | } 42 | elseif (count(explode(' ', $v->username)) > 1) 43 | { 44 | $output['error']['usernameSpace'][$v->id] = array('id' => $v->id, 'userid' => $v->userid, 'domain' => $v->domain, 'username' => $v->username, 'server' => $v->server); 45 | } 46 | else 47 | { 48 | $output['servers'][$v->server]['accounts'] = $i++; 49 | $temp[$v->server][] = array('id' => $v->id, 'userid' => $v->userid, 'username' => $v->username, 'external-id' => $externalID[$v->userid], 'domain' => $v->domain, 'server' => $v->server); 50 | } 51 | } 52 | 53 | $i = 0; 54 | 55 | if ($temp) 56 | { 57 | require_once('PleskAPIClient.php'); 58 | 59 | foreach ($temp as $serverID => $packages) 60 | { 61 | $plesk = new PleskApiClient($output['servers'][$serverID]['hostname']); 62 | $plesk->setCredentials($output['servers'][$serverID]['username'], $output['servers'][$serverID]['password']); 63 | 64 | $request .= << 66 | 67 | EOF; 68 | 69 | foreach ($packages as $package) 70 | { 71 | $request .= << 73 | 74 | {$package['username']} 75 | 76 | 77 | 78 | 79 | 80 | EOF; 81 | } 82 | 83 | $request .= << 85 | 86 | EOF; 87 | $response = $plesk->request($request); 88 | if (!$response): continue; endif; 89 | $response = new SimpleXMLElement($response); 90 | $response = json_decode(json_encode($response), true); 91 | 92 | foreach ($response['customer']['get'] as $k => $v) 93 | { 94 | if ($v['result']['errtext'] == 'client does not exist') 95 | { 96 | $output['error']['clientNotFound'][$temp[$serverID][$k]['userid']] = array('id' => $temp[$serverID][$k]['id'], 'userid' => $temp[$serverID][$k]['userid'], 'domain' => $temp[$serverID][$k]['domain'], 'username' => $temp[$serverID][$k]['username'], 'server' => $temp[$serverID][$k]['server']); 97 | } 98 | elseif (!$v['result']['data']['gen_info']['external-id'] AND $temp[$serverID][$k]['external-id']) 99 | { 100 | $hostingList = Capsule::table('tblhosting')->where('userid', $temp[$serverID][$k]['userid'])->where('server', $serverID)->pluck('domain', 'id'); 101 | $output['error']['externalID'][$serverID][$temp[$serverID][$k]['userid']] = array('userid' => $temp[$serverID][$k]['userid'], 'username' => $v['result']['filter-id'], 'external_id' => $temp[$serverID][$k]['external-id'], 'server' => $serverID, 'accounts' => $hostingList); 102 | $i++; 103 | } 104 | } 105 | 106 | unset($plesk, $request, $response); 107 | } 108 | 109 | $output['externalIDCount'] = $i; 110 | } 111 | 112 | return $output; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /modules/addons/PleskChecker/core/Katamaze/PleskAPIClient.php: -------------------------------------------------------------------------------- 1 | _host = $host; 26 | $this->_port = $port; 27 | $this->_protocol = $protocol; 28 | } 29 | 30 | /** 31 | * Setup credentials for authentication 32 | * 33 | * @param string $login 34 | * @param string $password 35 | */ 36 | public function setCredentials($login, $password) 37 | { 38 | $this->_login = $login; 39 | $this->_password = $password; 40 | } 41 | 42 | /** 43 | * Define secret key for alternative authentication 44 | * 45 | * @param string $secretKey 46 | */ 47 | public function setSecretKey($secretKey) 48 | { 49 | $this->_secretKey = $secretKey; 50 | } 51 | 52 | /** 53 | * Perform API request 54 | * 55 | * @param string $request 56 | * @return string 57 | */ 58 | public function request($request) 59 | { 60 | $curl = curl_init(); 61 | 62 | curl_setopt($curl, CURLOPT_URL, "$this->_protocol://$this->_host:$this->_port/enterprise/control/agent.php"); 63 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); 64 | curl_setopt($curl, CURLOPT_POST, true); 65 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, false); 66 | curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, false); 67 | curl_setopt($curl, CURLOPT_HTTPHEADER, $this->_getHeaders()); 68 | curl_setopt($curl, CURLOPT_POSTFIELDS, $request); 69 | 70 | $result = curl_exec($curl); 71 | 72 | curl_close($curl); 73 | 74 | return $result; 75 | } 76 | 77 | /** 78 | * Retrieve list of headers needed for request 79 | * 80 | * @return array 81 | */ 82 | private function _getHeaders() 83 | { 84 | $headers = array( 85 | "Content-Type: text/xml", 86 | "HTTP_PRETTY_PRINT: TRUE", 87 | ); 88 | 89 | if ($this->_secretKey) { 90 | $headers[] = "KEY: $this->_secretKey"; 91 | } else { 92 | $headers[] = "HTTP_AUTH_LOGIN: $this->_login"; 93 | $headers[] = "HTTP_AUTH_PASSWD: $this->_password"; 94 | } 95 | 96 | return $headers; 97 | } 98 | 99 | } 100 | -------------------------------------------------------------------------------- /modules/addons/PleskChecker/core/Katamaze/index.php: -------------------------------------------------------------------------------- 1 | 2 |
    3 | 8 |
    9 |
    10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | {foreach $checker.error.noDomain as $k => $v} 18 | 19 | 22 | 25 | 28 | 29 | {foreachelse} 30 | 31 | 32 | 33 | {/foreach} 34 | 35 |
    User IDHosting IDServer
    20 | #{$v.userid} 21 | 23 | #{$v.id} 24 | 26 | {$checker.servers[$v.server].hostname} 27 |
    No Issue Found
    36 |
    37 |
    38 |
    39 |
    40 | 45 |
    46 |
    47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | {foreach $checker.error.noUsername as $k => $v} 56 | 57 | 60 | 63 | 66 | 69 | 70 | {foreachelse} 71 | 72 | 73 | 74 | {/foreach} 75 | 76 |
    User IDHosting IDDomainServer
    58 | #{$v.userid} 59 | 61 | #{$v.id} 62 | 64 | {$v.domain} 65 | 67 | {$checker.servers[$v.server].hostname} 68 |
    No Issue Found
    77 |
    78 |
    79 |
    80 |
    81 | 86 |
    87 | 88 |
    89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | {foreach $checker.error.usernameSpace as $k => $v} 99 | 100 | 103 | 106 | 109 | 112 | 115 | 116 | {foreachelse} 117 | 118 | 119 | 120 | {/foreach} 121 | 122 |
    User IDHosting IDDomainUsernameServer
    101 | #{$v.userid} 102 | 104 | #{$v.id} 105 | 107 | {$v.domain} 108 | 110 | {$v.username|replace:' ':' '} 111 | 113 | {$checker.servers[$v.server].hostname} 114 |
    No Issue Found
    123 |
    124 |
    125 |
    126 |
    127 | 132 |
    133 |
    134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | {foreach $checker.error.externalID as $k => $v} 144 | 145 | 148 | 149 | 150 | {foreach $v as $subk => $subv} 151 | 152 | 153 | 156 | 161 | 166 | 169 | 170 | {/foreach} 171 | {foreachelse} 172 | 173 | 174 | 175 | {/foreach} 176 | 177 |
    ServerUser IDHostingDomainExternal ID
    146 | {$checker.servers[$k].hostname} 147 |
    154 | #{$subk} 155 | 157 | {foreach $subv.accounts as $id => $domain} 158 | #{$id}
    159 | {/foreach} 160 |
    162 | {foreach $subv.accounts as $id => $domain} 163 | {$domain}
    164 | {/foreach} 165 |
    167 | {$subv.external_id} 168 |
    No Issue Found
    178 |
    179 |
    180 |
    181 |
    182 | 187 |
    188 |
    189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | {foreach $checker.error.clientNotFound as $k => $v} 199 | 200 | 203 | 206 | 209 | 212 | 215 | 216 | {foreachelse} 217 | 218 | 219 | 220 | {/foreach} 221 | 222 |
    User IDHosting IDDomainUsernameServer
    201 | #{$v.userid} 202 | 204 | #{$v.id} 205 | 207 | {$v.domain} 208 | 210 | {$v.username} 211 | 213 | {$checker.servers[$v.server].hostname} 214 |
    No Issue Found
    223 |
    224 |
    225 |
    226 | 227 | -------------------------------------------------------------------------------- /modules/addons/PleskChecker/templates/Admin/index.php: -------------------------------------------------------------------------------- 1 | 10 | * 11 | */ 12 | 13 | use WHMCS\Carbon; 14 | use WHMCS\Database\Capsule; 15 | 16 | if (!defined('WHMCS')) { 17 | die('This file cannot be accessed directly'); 18 | } 19 | 20 | if (substr($GLOBALS['CONFIG']['Version'], 0, 1) === '8'): $v8 = true; endif; 21 | $dateFilter = Carbon::create($year, $month, 1); 22 | $startOfMonth = $dateFilter->startOfMonth()->toDateTimeString(); 23 | $endOfMonth = $dateFilter->endOfMonth()->toDateTimeString(); 24 | 25 | $reportdata['title'] = 'Churn Rate for ' . $year; 26 | $reportdata['description'] = 'Rate at which customers stop doing business with you.'; 27 | $reportdata['yearspagination'] = true; 28 | $reportdata['tableheadings'] = array( 29 | 'Date', 30 | 'Products', 31 | '', 32 | '', 33 | '', 34 | 'Domains', 35 | '', 36 | '', 37 | '', 38 | 'Overall', 39 | '', 40 | '', 41 | '', 42 | ); 43 | 44 | $reportvalues = array(); 45 | $mothMatrix = array('1' => '0', '2' => '0', '3' => '0', '4' => '0', '5' => '0', '6' => '0', '7' => '0', '8' => '0', '9' => '0', '10' => '0', '11' => '0', '12' => '0'); 46 | 47 | // Products/Services 48 | $groupBy = Capsule::raw('date_format(`regdate`, "%c")'); 49 | $products['active']['previousYears'] = Capsule::table('tblhosting')->whereYear('regdate', '<=', $year - 1)->whereYear('nextduedate', '>=', $year)->whereNotIn('billingcycle', ['One Time', 'Completed', 'Free Account'])->whereNotIn('domainstatus', ['Pending', 'Fraud'])->pluck(Capsule::raw('count(id) as total'))[0]; 50 | $products['active']['currentYear'] = Capsule::table('tblhosting')->whereYear('regdate', '=', $year)->whereYear('nextduedate', '>=', $year)->whereNotIn('billingcycle', ['One Time', 'Completed', 'Free Account'])->whereNotIn('domainstatus', ['Pending', 'Fraud'])->groupBy($groupBy)->pluck(Capsule::raw('count(id) as total'), Capsule::raw('date_format(`regdate`, "%c") as month')); 51 | if ($v8): $products['active']['currentYear'] = $products['active']['currentYear']->all(); endif; 52 | $products['active']['currentYear'] = $products['active']['currentYear'] + $mothMatrix; 53 | ksort($products['active']['currentYear']); 54 | $products['active']['total'] = $products['active']['previousYears'] + array_sum($products['active']['currentYear']); 55 | $groupBy = Capsule::raw('date_format(`nextduedate`, "%c")'); 56 | $products['terminated'] = Capsule::table('tblhosting')->whereYear('nextduedate', '=', $year)->whereIn('domainstatus', ['Suspended', 'Terminated', 'Cancelled'])->whereNotIn('billingcycle', ['One Time', 'Completed', 'Free Account'])->groupBy($groupBy)->orderBy('nextduedate')->pluck(Capsule::raw('count(id) as total'), Capsule::raw('date_format(`nextduedate`, "%c") as month')); 57 | if ($v8): $products['terminated'] = $products['terminated']->all(); endif; 58 | $products['terminated'] = $products['terminated'] + $mothMatrix; 59 | ksort($products['terminated']); 60 | $products['variation'] = array_map('subtract', $products['active']['currentYear'], $products['terminated']); 61 | $products['variation'] = array_combine(range(1, count($products['variation'])), array_values($products['variation'])); 62 | 63 | // Domains 64 | $groupBy = Capsule::raw('date_format(`registrationdate`, "%c")'); 65 | $domains['active']['previousYears'] = Capsule::table('tbldomains')->whereYear('registrationdate', '<=', $year - 1)->whereYear('nextduedate', '>=', $year)->whereNotIn('status', ['Pending', 'Pending Registration', 'Pending Transfer', 'Fraud'])->pluck(Capsule::raw('count(id) as total'))[0]; 66 | $domains['active']['currentYear'] = Capsule::table('tbldomains')->whereYear('registrationdate', '=', $year)->whereYear('nextduedate', '>=', $year)->whereNotIn('status', ['Pending', 'Pending Registration', 'Pending Transfer', 'Fraud'])->groupBy($groupBy)->pluck(Capsule::raw('count(id) as total'), Capsule::raw('date_format(`registrationdate`, "%c") as month')); 67 | if ($v8): $domains['active']['currentYear'] = $domains['active']['currentYear']->all(); endif; 68 | $domains['active']['currentYear'] = $domains['active']['currentYear'] + $mothMatrix; 69 | ksort($domains['active']['currentYear']); 70 | $domains['active']['total'] = $domains['active']['previousYears'] + array_sum($domains['active']['currentYear']); 71 | $groupBy = Capsule::raw('date_format(`nextduedate`, "%c")'); 72 | $domains['terminated'] = Capsule::table('tbldomains')->whereYear('nextduedate', '=', $year)->whereIn('status', ['Grace', 'Redemption', 'Expired', 'Transferred Away', 'Cancelled'])->groupBy($groupBy)->orderBy('nextduedate')->pluck(Capsule::raw('count(id) as total'), Capsule::raw('date_format(`nextduedate`, "%c") as month')); 73 | if ($v8): $domains['terminated'] = $domains['terminated']->all(); endif; 74 | $domains['terminated'] = $domains['terminated'] + $mothMatrix; 75 | ksort($domains['terminated']); 76 | $domains['variation'] = array_map('subtract', $domains['active']['currentYear'], $domains['terminated']); 77 | $domains['variation'] = array_combine(range(1, count($domains['variation'])), array_values($domains['variation'])); 78 | 79 | // Domains 80 | $groupBy = Capsule::raw('date_format(`registrationdate`, "%c")'); 81 | $reportvalues['domainsNew'] = Capsule::table('tbldomains')->where('status', 'Active')->whereYear('registrationdate', '=', $year)->groupBy($groupBy)->orderBy('registrationdate')->pluck(Capsule::raw('count(id) as total'), Capsule::raw('date_format(`registrationdate`, "%c") as month')); 82 | $groupBy = Capsule::raw('date_format(`nextduedate`, "%c")'); 83 | $reportvalues['domainsTerminated'] = Capsule::table('tbldomains')->whereYear('nextduedate', '=', $year)->where('nextduedate', '<=', $dateFilter->format('Y-m-d'))->groupBy($groupBy)->orderBy('nextduedate')->pluck(Capsule::raw('count(id) as total'), Capsule::raw('date_format(`nextduedate`, "%c") as month')); 84 | $activeDomains = Capsule::table('tbldomains')->where('status', 'Active')->pluck(Capsule::raw('count(id) as total'))[0]; 85 | 86 | for ($tmonth = 1; $tmonth <= 12; $tmonth++) 87 | { 88 | if (date('Y') == $year AND sprintf("%02d", $tmonth) > $month): continue; endif; 89 | 90 | $date = Carbon::create($year, $tmonth, 1); 91 | $dateMonthYear = $date->format('M Y'); 92 | $dateMonth = $date->format('M'); 93 | 94 | // Products 95 | if ($tmonth == '1') 96 | { 97 | $products['start'][$tmonth] = $products['active']['previousYears'] + $products['start'][$tmonth]; 98 | $products['end'][$tmonth] = $products['start'][$tmonth] + $products['variation'][$tmonth]; 99 | } 100 | else 101 | { 102 | $products['start'][$tmonth] = $products['end'][$tmonth - 1]; 103 | $products['end'][$tmonth] = $products['start'][$tmonth] + $products['variation'][$tmonth]; 104 | } 105 | 106 | // Domains 107 | if ($tmonth == '1') 108 | { 109 | $domains['start'][$tmonth] = $domains['active']['previousYears'] + $domains['start'][$tmonth]; 110 | $domains['end'][$tmonth] = $domains['start'][$tmonth] + $domains['variation'][$tmonth]; 111 | } 112 | else 113 | { 114 | $domains['start'][$tmonth] = $domains['end'][$tmonth - 1]; 115 | $domains['end'][$tmonth] = $domains['start'][$tmonth] + $domains['variation'][$tmonth]; 116 | } 117 | 118 | $productsChurnRate = number_format(($products['terminated'][$tmonth] / $products['start'][$tmonth]) * 100, 1, '.', '') + 0; 119 | $domainsChurnRate = number_format(($domains['terminated'][$tmonth] / $domains['start'][$tmonth]) * 100, 1, '.', '') + 0; 120 | 121 | if (sprintf("%02d", $tmonth) > $month AND date('Y') == $year) 122 | { 123 | $dateMonthYear = '' . $dateMonthYear . ' '; 124 | } 125 | 126 | $reportdata['tablevalues'][] = array( 127 | $dateMonthYear, 128 | formatCell(array('col' => 'products=', 'variation' => $products['variation'][$tmonth], 'start' => $products['start'][$tmonth], 'end' => $products['end'][$tmonth])), 129 | formatCell(array('col' => 'products+', 'increase' => $products['active']['currentYear'][$tmonth])), 130 | formatCell(array('col' => 'products-', 'decrease' => $products['terminated'][$tmonth])), 131 | formatCell(array('col' => 'products%', 'churnRate' => $productsChurnRate)), 132 | formatCell(array('col' => 'domains=', 'variation' => $domains['variation'][$tmonth], 'start' => $domains['start'][$tmonth], 'end' => $domains['end'][$tmonth])), 133 | formatCell(array('col' => 'domains+', 'increase' => $domains['active']['currentYear'][$tmonth])), 134 | formatCell(array('col' => 'domains-', 'decrease' => $domains['terminated'][$tmonth])), 135 | formatCell(array('col' => 'domains%', 'churnRate' => $domainsChurnRate)), 136 | formatCell(array('col' => 'overall=', 'variation' => $products['variation'][$tmonth] + $domains['variation'][$tmonth], 'start' => $products['start'][$tmonth] + $domains['start'][$tmonth], 'end' => $products['end'][$tmonth] + $domains['end'][$tmonth])), 137 | formatCell(array('col' => 'overall+', 'increase' => $products['active']['currentYear'][$tmonth] + $domains['active']['currentYear'][$tmonth])), 138 | formatCell(array('col' => 'overall-', 'decrease' => $products['terminated'][$tmonth] + $domains['terminated'][$tmonth])), 139 | formatCell(array('col' => 'overall%', 'churnRate' => $productsChurnRate + $domainsChurnRate)) 140 | ); 141 | 142 | $chartdata['rows'][] = array( 143 | 'c'=>array( 144 | array('v' => $dateMonth), 145 | array('v' => (int)$products['end'][$tmonth]), 146 | array('v' => (int)$domains['end'][$tmonth]), 147 | array('v' => (int)$products['end'][$tmonth] + $domains['end'][$tmonth]), 148 | ) 149 | ); 150 | } 151 | 152 | function formatCell($data) 153 | { 154 | /** 155 | * @param string $col Column type 156 | * @param string $variation Monthly change 157 | * @param string $increase New purchases 158 | * @param string $decrease New terminations 159 | * @param string $start No. of products/services (at the start of the period) 160 | * @param string $end No. of products/services (at the end of the period) 161 | * @param string $churnRate Churn Rate 162 | * @return string Formatted HTML cell 163 | */ 164 | $data['variation'] = ($data['variation'] ? $data['variation'] : '0'); 165 | $data['increase'] = ($data['increase'] ? $data['increase'] : '0'); 166 | $data['decrease'] = ($data['decrease'] ? $data['decrease'] : '0'); 167 | $data['start'] = ($data['start'] ? $data['start'] : '0'); 168 | $data['end'] = ($data['end'] ? $data['end'] : '0'); 169 | $data['churnRate'] = ($data['churnRate'] ? $data['churnRate'] : false); 170 | 171 | if (in_array($data['col'], array('products+', 'domains+', 'overall+'))) 172 | { 173 | if ($data['increase']) 174 | { 175 | return '' . $data['increase'] . ''; 176 | } 177 | else 178 | { 179 | return '-'; 180 | } 181 | } 182 | elseif (in_array($data['col'], array('products-', 'domains-', 'overall-'))) 183 | { 184 | if ($data['decrease']) 185 | { 186 | return '' . $data['decrease'] . ''; 187 | } 188 | else 189 | { 190 | return '-'; 191 | } 192 | } 193 | elseif (in_array($data['col'], array('products=', 'domains=', 'overall='))) 194 | { 195 | if ($data['variation'] > '0') 196 | { 197 | $variation = '' . abs($data['variation']) . ''; 198 | } 199 | elseif ($data['variation'] < '0') 200 | { 201 | $variation = '' . abs($data['variation']) . ''; 202 | } 203 | 204 | if ($data['start'] != $data['end']) 205 | { 206 | return $data['start'] . ' ' . $data['end'] . $variation; 207 | } 208 | else 209 | { 210 | return $data['start']; 211 | } 212 | } 213 | elseif (in_array($data['col'], array('products%', 'domains%', 'overall%'))) 214 | { 215 | if ($data['churnRate'] > 0) 216 | { 217 | return '' . $data['churnRate'] . '%'; 218 | } 219 | else 220 | { 221 | return '-'; 222 | } 223 | } 224 | } 225 | 226 | function subtract($a, $b) 227 | { 228 | return $a - $b; 229 | } 230 | 231 | $chartdata['cols'][] = array('label'=>'Day','type'=>'string'); 232 | $chartdata['cols'][] = array('label'=>'Products','type'=>'number'); 233 | $chartdata['cols'][] = array('label'=>'Domains','type'=>'number'); 234 | $chartdata['cols'][] = array('label'=>'Overall','type'=>'number'); 235 | 236 | $args = array(); 237 | $args['legendpos'] = 'right'; 238 | 239 | $reportdata["headertext"] = $chart->drawChart('Area',$chartdata,$args,'400px'); 240 | --------------------------------------------------------------------------------