1:   2:   3:   4:   5:   6:   7:   8:   9:  10:  11:  12:  13:  14:  15:  16:  17:  18:  19:  20:  21:  22:  23:  24:  25:  26:  27:  28:  29:  30:  31:  32:  33:  34:  35:  36:  37:  38:  39:  40:  41:  42:  43:  44:  45:  46:  47:  48:  49:  50:  51:  52:  53:  54:  55:  56:  57:  58:  59:  60:  61:  62:  63:  64:  65:  66:  67:  68:  69:  70:  71:  72:  73:  74:  75:  76:  77:  78:  79:  80:  81:  82:  83:  84:  85:  86:  87:  88:  89:  90:  91:  92:  93:  94:  95:  96:  97:  98:  99: 100: 101: 102: 103: 104: 105: 106: 107: 108: 109: 110: 111: 112: 113: 114: 115: 116: 117: 118: 119: 120: 121: 122: 123: 124: 125: 126: 127: 128: 129: 130: 131: 132: 133: 134: 135: 136: 137: 138: 139: 140: 141: 142: 143: 144: 145: 146: 147: 148: 149: 150: 151: 152: 153: 154: 155: 156: 157: 158: 159: 160: 161: 162: 163: 164: 165: 166: 167: 168: 169: 170: 171: 172: 173: 174: 175: 176: 177: 178: 179: 180: 181: 182: 183: 184: 185: 186: 187: 188: 189: 190: 191: 192: 193: 194: 195: 196: 197: 198: 199: 200: 201: 202: 203: 204: 205: 206: 207: 208: 209: 210: 211: 212: 213: 214: 215: 216: 217: 218: 219: 220: 221: 222: 223: 224: 225: 226: 227: 228: 229: 230: 231: 232: 233: 234: 235: 236: 237: 238: 239: 240: 241: 242: 243: 244: 245: 246: 247: 248: 249: 250: 251: 252: 253: 254: 255: 256: 257: 258: 259: 260: 261: 262: 263: 264: 265: 266: 267: 268: 269: 270: 271: 272: 273: 274: 275: 276: 277: 278: 279: 280: 281: 282: 283: 284: 285: 286: 287: 288: 289: 290: 291: 292: 293: 294: 295: 296: 297: 298: 299: 300: 301: 302: 303: 304: 305: 306: 307: 308: 309: 310: 
<?php
    declare(strict_types=1);
    /**
     *  +------------------------------------------------------------+
     *  | apnscp                                                     |
     *  +------------------------------------------------------------+
     *  | Copyright (c) Apis Networks                                |
     *  +------------------------------------------------------------+
     *  | Licensed under Artistic License 2.0                        |
     *  +------------------------------------------------------------+
     *  | Author: Matt Saladna (msaladna@apisnetworks.com)           |
     *  +------------------------------------------------------------+
     */

    use Opcenter\Bandwidth\Site;

    /**
     * Bandwidth statistics
     *
     * @package core
     */
    class Bandwidth_Module extends Module_Skeleton implements \Opcenter\Contracts\Hookable
    {
        const DEPENDENCY_MAP = [
            'siteinfo',
            'apache'
        ];
        const BW_DIR = '/var/log/bw';

        protected $exportedFunctions = [
            '*' => PRIVILEGE_SITE,
            'amnesty' => PRIVILEGE_ADMIN
        ];

        /**
         * Get bandwidth consumed by period
         *
         * @param $grouping string group bandwidth by 'month' or 'day'
         * @return array|false
         */
        public function get_all_composite_bandwidth_data($grouping = 'month')
        {
            if (!\in_array($grouping, ['month', 'day'])) {
                return error("invalid bw grouping `%s'", $grouping);
            }

            $cachekey = 'bandwidth.gacbd.' . substr($grouping, 0, 2);

            $cache = Cache_Account::spawn($this->getAuthContext());
            $bw = $cache->get($cachekey);
            if ($bw !== false) {
                return $bw;
            }
            $bandwidth = (new Site($this->site_id))->getByComposite($grouping);
            $cache->set($cachekey, $bandwidth, 1800);

            return $bandwidth;
        }

        /**
         *  Get bandwidth ranges
         *  Indexes:
         *      begin: start starting rollover date
         *      end:   ending rollover date
         *
         * @return array
         */
        public function get_cycle_periods(): array
        {

            $cachekey = 'bandwidth.gcp';
            $cache = Cache_Account::spawn($this->getAuthContext());
            $periods = $cache->get($cachekey);
            if ($periods !== false) {
                return $periods;
            }

            $periods = (new Site($this->site_id))->rollovers();

            $cache->set($cachekey, $periods, 43200);

            return $periods;
        }

        /**
         * Retrieve day on which banwidth rolls over to 0
         *
         * @return int
         */
        public function rollover(): int
        {
            $rollover = (int)$this->getServiceValue('bandwidth', 'rollover');
            $localtime = localtime(time(), true);
            $today = date('j');
            $month = ($rollover < $today ? ($localtime['tm_mon'] + 1) : $localtime['tm_mon']);

            return mktime(0, 0, 0, ++$month, $rollover);
        }

        /**
         * Get cumulative bandwidth consumption
         *
         * @privilege PRIVILEGE_SITE
         * @param int $type type of bandwidth usage to retrieve
         * @return array|bool indexes begin, rollover, and threshold
         */
        public function usage(int $type = null)
        {
            if (!$site_id = $this->site_id) {
                Error_Reporter::report(var_export($this, true));

                return false;
            }
            if (!$this->bandwidth_enabled()) {
                return ['used' => 0, 'total' => -1];
            }
            $pgdb = \PostgreSQL::initialize();
            switch ($type) {
                case null:
                    $bw_rs = $pgdb->query('SELECT
                        begindate,
                        rollover,
                        threshold as threshold
                        FROM bandwidth_spans
                        JOIN bandwidth USING (site_id)
                        WHERE
                        bandwidth_spans.site_id = ' . $site_id . '
                        AND
                        bandwidth_spans.enddate IS NULL
                        AND
                        bandwidth.site_id = ' . $site_id);
                    if (!$bw_rs->num_rows()) {
                        if ($this->enabled()) {
                            // no span present, hotfix for next request
                            // ideally, recurse but bandwidth record isn't set through here
                            $this->_autofix_bandwidth($site_id, (int)$this->getServiceValue('bandwidth', 'rollover'));
                        }
                        return array('used' => 0, 'total' => -1);
                    }
                    $bw_rs = $bw_rs->fetch_object();
                    if (!$bw_rs) {
                        return [
                            'used'  => 0,
                            'total' => -1
                        ];
                    }
                    // @BUG account has no bandwidth enddate
                    if ($bw_rs && !$bw_rs->begindate && $this->enabled()) {
                        $ret = $this->_autofix_bandwidth($site_id,
                            (int)$this->getServiceValue('bandwidth', 'rollover'));
                        if (!$ret) {
                            return error("failed to autofix bandwidth for site `%d'", $this->site_id);
                        }

                        return $this->usage($type);
                    }

                    $used_rs = $pgdb->query('SELECT
                        SUM(in_bytes)+SUM(out_bytes) AS sum
                        FROM
                        bandwidth_log
                        WHERE
                        site_id = ' . $this->site_id . "
                        AND
                        ts >= '" . $bw_rs->begindate . "'");

                    return array(
                        'used'  => (float)$used_rs->fetch_object()->sum,
                        'total' => (float)($bw_rs ? $bw_rs->threshold : -1)
                    );
                    break;
                default:
                    return error("Unknown bandwidth classifier `%d'", $type);
            }
        }

        /* }}} */


        /**
         * Fill in missing spans for bandwidth
         *
         * @param int $site_id
         * @param int $rollover
         * @return bool
         * @throws PostgreSQLError
         */
        private function _autofix_bandwidth(int $site_id, int $rollover): bool
        {
            $db = \PostgreSQL::initialize();
            $ts = mktime(0, 0, 0, (int)date('m'), $rollover);
            $ts2 = strtotime('last month', $ts);
            if ($ts > time()) {
                $ts = $ts2;
            }
            $db->query("INSERT INTO bandwidth_spans (site_id, begindate, enddate) VALUES($site_id, TO_TIMESTAMP($ts), NULL)
                ON CONFLICT (site_id,begindate) DO NOTHING;");

            return !(bool)pg_last_error();
        }

        /**
         * Get bandwidth consumed during a time interval
         *
         * @param int $begin beginning date
         * @param int $end   ending date
         * @return array|bool
         */
        public function get_by_date(int $begin, int $end = null)
        {
            if (!$begin) {
                return error('no begin period set');
            } else if ($begin < 0 || $end < 0) {
                return error('Invalid begin or end date');
            } else if ($end && $begin > $end) {
                return error('Begin date may not be before end date');
            }
            // there may be collisions, but sacrifice for key len
            $cachekey = 'bandwidth.gbd.' . crc32($begin . $end);
            $cache = Cache_Account::spawn($this->getAuthContext());
            $services = $cache->get($cachekey);
            if ($services !== false) {
                return $services;
            }
            $services = (new Site($this->site_id))->getByRange($begin, $end);
            $cache->set($cachekey, $services, 43200);

            return $services;
        }

        public function enabled(): bool
        {
            return (bool)$this->getServiceValue('bandwidth', 'enabled');
        }

        /**
         * Grant bandwidth amnesty until closing date
         *
         * @param string $marker
         * @return bool
         */
        public function amnesty(string $marker): bool
        {
            if (null === ($siteId = \Auth::get_site_id_from_anything($marker))) {
                return error("Unable to resolve site from `%s'", $marker);
            }
            $bwhandler = new Site($siteId);
            info(
                'Amnesty granted on site%d until %s',
                $siteId,
                date('r', $bwhandler->getCycleEnd())
            );
            return $bwhandler->amnesty();
        }

        public function _delete()
        {
            $glob = self::BW_DIR . '/*/' . $this->site . '{,.?}';
            foreach (glob($glob) as $f) {
                unlink($f);
            }
        }

        public function _edit()
        {
            $conf_new = $this->getAuthContext()->getAccount()->new;
            $conf_old = $this->getAuthContext()->getAccount()->old;
            $user = array(
                'old' => $conf_old['siteinfo']['admin_user'],
                'new' => $conf_new['siteinfo']['admin_user']
            );
            if ($user['old'] !== $user['new']) {
                (new Site($this->site_id))->renameExtendedInfo($user['old'], $user['new']);
            }
        }

        public function _edit_user(string $userold, string $usernew, array $oldpwd)
        {
            if ($userold === $usernew) {
                return;
            }
            // update
            (new Site($this->site_id))->renameExtendedInfo($userold, $usernew);

            return true;
        }

        public function _verify_conf(\Opcenter\Service\ConfigurationContext $ctx): bool
        {
            return true;
        }

        public function _create()
        {
            // TODO: Implement _create() method.
        }

        public function _create_user(string $user)
        {
            // TODO: Implement _create_user() method.
        }

        public function _delete_user(string $user)
        {
            // TODO: Implement _delete_user() method.
        }


    }