phplib/Notification.php

Summary

Maintainability
C
7 hrs
Test Coverage
<?php

namespace FOO;

/**
 * Class Notification
 * Generates and sends various email notifications.
 * @package FOO
 */
class Notification {

    /**
     * Send an email about new Alerts.
     * @param string[]|string $to The destination email.
     * @param Search $search The Search object.
     * @param Alert[] $alerts The list of Alerts.
     * @param boolean $content_only Whether to hide metadata.
     * @param array $debug_data Watermarking data for debugging purposes.
     * @throws \Exception
     */
    public static function sendAlertEmail($to, $search, $alerts, $content_only, $debug_data=[]) {
        $alertkeys = [];
        $long = false;
        foreach($alerts as $alert) {
            if(count($alert['content']) >= 10) {
                $long = true;
            }
            foreach($alert['content'] as $k=>$v) {
                $alertkeys[$k] = null;
                if(strlen($v) > 150) {
                    $long = true;
                }
            }
        }
        $alertkeys = array_keys($alertkeys);

        self::mail(
            $to, self::getFrom(),
            $search['name'],
            self::render('alerts', [
                'search' => $search,
                'alerts' => self::renderAlerts($search, $alerts),
                'alertkeys' => $alertkeys,
                'content_only' => $content_only,
                'vertical' => $long,
            ], $debug_data)
        );
    }

    /**
     * Send an email about new actions on Alerts.
     * @param string[]|string $to The destination email.
     * @param AlertLog $action The action taken.
     * @param array $searches A mapping of search ids to Searches.
     * @param Alert[] $alerts The list of Alerts.
     * @param boolean $content_only Whether to hide metadata.
     * @param array $debug_data Watermarking data for debugging purposes.
     * @throws \Exception
     */
    public static function sendAlertActionEmail($to, $action, $searches, $alerts, $content_only=false, $debug_data=[]) {
        self::mail(
            $to, self::getFrom(),
            $action->getDescription(),
            self::render('action', [
                'action' => $action,
                'alert_groups' => self::groupAlerts($searches, $alerts),
                'content_only' => $content_only,
                'vertical' => false,
            ], $debug_data)
        );
    }

    /**
     * Send an email rollup about new Alerts and actions taken.
     * @param string[]|string $to The destination email.
     * @param Alert[] $new_alerts The list of new Alerts.
     * @param AlertLog[] $actions The list of actions.
     * @param Search[] $searches The list of Searches.
     * @param Alert[] $action_alerts The list of Alerts that have been actioned on.
     * @param array $active_alerts The counts of active Alerts.
     * @param boolean $content_only Whether to hide metadata.
     * @param array $debug_data Watermarking data for debugging purposes.
     * @throws \Exception
     */
    public static function sendRollupEmail($to, $new_alerts, $actions, $searches, $action_alerts, $active_alerts, $content_only=false, $debug_data=[]) {
        $search_map = [];
        foreach($searches as $search) {
            $search_map[$search['id']] = $search;
        }

        self::mail(
            $to, self::getFrom(),
            sprintf(
                'Rollup [%d Alert%s] [%d Action%s]',
                count($new_alerts),
                count($new_alerts) != 1 ? 's':'',
                count($actions),
                count($actions) != 1 ? 's':''
            ),
            self::render('rollup', [
                'new_count' => $active_alerts[0],
                'inprog_count' => $active_alerts[1],
                'new_alert_groups' => self::groupAlerts($search_map, $new_alerts),
                'actions' => $actions,
                'action_alert_groups' => self::groupAlerts($search_map, $action_alerts),
                'content_only' => $content_only,
                'vertical' => false,
            ], $debug_data)
        );
    }

    /**
     * Send an email about a failing Search type.
     * @param string[]|string $to The destination email.
     * @param string $type The Search type.
     * @param array $debug_data Watermarking data for debugging purposes.
     * @throws \Exception
     */
    public static function sendSearchTypeErrorEmail($to, $type, $debug_data=[]) {
        self::mail(
            $to, self::getFrom(true),
            sprintf('[Failure] "%s" Search Type', $type),
            self::render('searchtypeerror', [
                'type' => $type
            ], $debug_data)
        );
    }

    /**
     * Send an email about a recovered Search type.
     * @param string[]|string $to The destination email.
     * @param string $type The Search type.
     * @param array $debug_data Watermarking data for debugging purposes.
     * @throws \Exception
     */
    public static function sendSearchTypeRecoveryEmail($to, $type, $debug_data=[]) {
        self::mail(
            $to, self::getFrom(true),
            sprintf('[Recovery] "%s" Search Type', $type),
            self::render('searchtyperecovery', [
                'type' => $type
            ], $debug_data)
        );
    }

    /**
     * Send an email about a failed Search.
     * @param string[]|string $to The destination email.
     * @param Search $search The Search object.
     * @param string[] $errors The list of errors.
     * @param array $debug_data Watermarking data for debugging purposes.
     * @throws \Exception
     */
    public static function sendSearchErrorEmail($to, $search, $errors, $debug_data=[]) {
        self::mail(
            $to, self::getFrom(true),
            sprintf('[Failure] "%s" Search', $search['name']),
            self::render('searcherror', [
                'search' => $search,
                'errors' => $errors
            ], $debug_data)
        );
    }

    /**
     * Send an email about a recovered Search.
     * @param string[]|string $to The destination email.
     * @param Search $search The Search object.
     * @param array $debug_data Watermarking data for debugging purposes.
     * @throws \Exception
     */
    public static function sendSearchRecoveryEmail($to, $search, $debug_data=[]) {
        self::mail(
            $to, self::getFrom(true),
            sprintf('[Recovery] "%s" Search', $search['name']),
            self::render('searchrecovery', [
                'search' => $search
            ], $debug_data)
        );
    }

    /**
     * Send an email with a weekly summary.
     * @param string[]|string $to The destination email.
     * @param \DateTime $start_date The starting date for this week.
     * @param int[] $stats New alerts, closed alerts and open alerts.
     * @param array $leaders Users who've closed the most Alerts this week w/ a count.
     * @param array $noisy_searches Searches that generate the most Alerts w/ a count.
     * @param array $quiet_searches Searches that generate the least Alerts w/ a count.
     * @param array $debug_data Watermarking data for debugging purposes.
     */
    public static function sendSummaryEmail($to, $start_date, $stats, $leaders, $noisy_searches, $quiet_searches, $debug_data=[]) {
        $end_date = clone $start_date;
        $end_date->modify('+6 days');
        self::mail(
            $to, self::getFrom(),
            sprintf('Weekly summary: %s to %s', $start_date->format('Y-m-d'), $end_date->format('Y-m-d')),
            self::render('summary', [
                'new_count' => $stats[0],
                'close_count' => $stats[1],
                'open_count' => $stats[2],
                'leaders' => $leaders,
                'noisy_searches' => $noisy_searches,
                'quiet_searches' => $quiet_searches,
            ], $debug_data)
        );
    }

    /**
     * Group Alerts underneath a Search.
     * @param Search $search The Search object.
     * @param Alert[] $alerts The list of Alerts.
     * @return array A keyed mapping of Alerts.
     */
    public static function renderAlerts(Search $search, array $alerts) {
        $ret = [];
        foreach($alerts as $alert) {
            $data = $alert->toArray();
            $data['source'] = $search->getLink($alert);
            $data['content'] = (array) $data['content'];
            foreach($alert['content'] as $k=>$v) {
                $type_list = Util::get($search['renderer_data'], $k, ['null']);
                $type = count($type_list) > 0 ? $type_list[0]:'null';
                $enricher = Enricher::getEnricher($type);
                $data['content'][$k] = $enricher::processHTML($v);
            }
            $ret[] = $data;
        }
        return $ret;
    }

    /**
     * Group Alerts underneath a Search.
     * @param array $search_map The Search mapping.
     * @param Alert[] $alerts The list of Alerts.
     * @return array A keyed mapping of Alerts.
     */
    private static function groupAlerts($search_map, array $alerts) {
        $groups = [];
        foreach($alerts as $alert) {
            if(!array_key_exists($alert['search_id'], $groups)) {
                $groups[$alert['search_id']] = [[], []];
            }

            $search = Util::get($search_map, $alert['search_id']);
            $data = $alert->toArray();
            $data['source'] = $search->getLink($alert);
            $data['content'] = (array) $data['content'];
            foreach($data['content'] as $k=>$v) {
                $enricher = Enricher::getEnricher(Util::get($search['renderer_data'], $k, 'null'));
                $data['content'][$k] = $enricher::processHTML($v);
                $groups[$alert['search_id']][1][$k] = null;
            }
            $groups[$alert['search_id']][0][] = $data;
        }

        $ret = [];
        foreach($groups as $key=>$data) {
            $search = Util::get($search_map, $key);
            $ret[] = [$search, $data[0], array_keys($data[1])];
        }
        return $ret;
    }

    /**
     * Get the source email address.
     * @param bool $error Whether this email is for an error.
     * @return string The email address.
     */
    public static function getFrom($error=false) {
        $cfg = new DBConfig();
        return $cfg[$error ? 'from_error_email':'from_email'] ?: sprintf('411@%s', Util::getHost());
    }

    /**
     * Renders a template.
     * @param string $tpl The name of the template.
     * @param array $vars Variables to inject into the template.
     * @param array $debug_data Watermarking data for debugging purposes.
     * @return string A templated string.
     * @throws \Exception
     */
    public static function render($tpl, $vars, $debug_data=[]) {
        $site = SiteFinder::getCurrent();

        // Embedded CSS. This is unfortunately necessary as most (all?) mail clients only allow inline CSS.
        $font = "font-family: 'Myriad Pro','Helvetica Neue',Helvetica,Tahoma,Arial,sans-serif;";
        $base_url = $site->urlFor('/');
        $large_style = "font-size: 1.6em;";
        $panel_style = "border-radius: 3px; background-color: #f5f5f5; color: #333; border: 1px solid #ddd;";
        $panel_content_style = "padding: 0px 15px; margin: 5px 0px; text-size: 1.5em;";
        $sub_style = "float: right; color: #777;";
        $well_style = "padding: 9px; border-radius: 3px; background: #f5f5f5; border: 1px solid #e3e3e3; box-shadow: inset 0 1px 1px rgba(0,0,0,.05);";
        $table_container_style = ""; // "overflow-x: auto";
        $table_style = "border-collapse: collapse; min-width: 100%;";
        $link_style = "color: #000;";
        $h_cell_style = "background-color: #eee; color: #333; border-top: 1px solid #ddd; padding: 5px;";
        $sp_cell_style = "background-color: #f5f5f5; border-top: 1px solid #ddd; padding: 5px;";
        $cell_style = "background-color: #f9f9f9; color: #333; border-top: 1px solid #ddd; padding: 5px; text-align: right;";
        $button_style = "padding: 1px 5px; border-radius: 3px; border: solid 1px #269abc; background: #5bc0de; color: #fff; text-decoration: none;";
        $action_button_style = "padding: 10px 16px; border-radius: 6px; border: solid 1px #4cae4c; background: #5cb85c; color: #fff; text-decoration: none; font-size: 18px; cursor: hand;";
        $info_alert_style = "color: #31708f; background-color: #d9edf7; border-color: #bce8f1; border-radius: 4px; padding: 15px;";
        $error_alert_style = "color: #a94442; background-color: #f2dede; border-color: #ebccd1; border-radius: 4px; padding: 15px;";

        // Extract variables into the current namespace ONLY if it doesn't already exist.
        extract($vars, EXTR_SKIP);

        // Render the template. If there's an error, dump the output buffer.
        ob_start();
        $template_file_list = [
            sprintf('%s/exttemplates/%s.php', BASE_DIR, $tpl),
            sprintf('%s/templates/%s.php', BASE_DIR, $tpl),
        ];
        try {
            foreach($template_file_list as $template_file) {
                if(file_exists($template_file)) {
                    require($template_file);
                    break;
                }
            }
        } catch(\Exception $e) {
            ob_end_clean();
            throw $e;
        }
        print sprintf("\n<!-- DEBUG\n%s\n-->", str_replace('&', "\n", http_build_query($debug_data)));
        return ob_get_clean();
    }

    /**
     * Render and send an email.
     * @param string[]|string $to Email addresses to send to.
     * @param string $from Email addresses to send from.
     * @param string $title The subject line.
     * @param string $message The message.
     * @param string $file The contents of a file to send.
     */
    public static function mail($to, $from, $title, $message, $file=null) {
        list($to, $from, $title, $message, $file) = Hook::call('mail', [$to, $from, $title, $message, $file]);
        $to = (array) $to;

        $bnd = uniqid();
        $headers = "From: $from\r\n";
        $headers.= "Reply-To: $to[0]\r\n";
        $headers.= "MIME-Version: 1.0\r\n";
        if (count($to) > 1) {
            $headers .= sprintf("CC: %s\r\n", implode(', ', array_slice($to, 1)));
        }
        $headers.= "Content-Type: multipart/mixed; boundary=$bnd\r\n\r\n";

        $sep = "--$bnd\r\n";
        $end = "--$bnd--";

        $body = $sep;
        $body.= "Content-Type: text/html; charset=utf-8\r\n";
        $body.= "Content-Transfer-Encoding: 8bit\r\n";
        $body.= "\r\n$message\r\n\r\n";

        if(!is_null($file)) {
            $body.= $sep;
            $body.= "Content-Type: application/octet-stream; name=\"report.pdf\"\r\n";
            $body.= "Content-Transfer-Encoding: base64\r\n";
            $body.= "Content-Disposition: attachment\r\n";
            $body.= "\r\n" . chunk_split(base64_encode($file)) . "\r\n\r\n";
        }
        $body.= $end;

        mail(
            $to[0],
            sprintf('[%s] %s', Util::getSiteName(), $title),
            $body,
            $headers
        );
    }
}