diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4b36487369b4ba838b937a4a8194644299b55c96..9a5a0312c1fc8aa286a2b4e3a324cd33aead7183 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -39,13 +39,11 @@ bundle_app: script: # build all assets for views - npm run build - # download GeoLite2-Country and opawg/user-agents archives and extract them to writable/uploads - - wget -c "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-Country&license_key=$MAXMIND_LICENCE_KEY&suffix=tar.gz" -O - | tar -xz -C ./writable/uploads/ - - wget -c "https://github.com/opawg/user-agents/archive/master.tar.gz" -O - | tar -xz -C ./writable/uploads/ + # download GeoLite2-City archive and extract it to writable/uploads + - wget -c "https://download.maxmind.com/app/geoip_download?edition_id=GeoLite2-City&license_key=$MAXMIND_LICENCE_KEY&suffix=tar.gz" -O - | tar -xz -C ./writable/uploads/ # rename extracted archives' folders - - mv ./writable/uploads/GeoLite2-Country* ./writable/uploads/GeoLite2-Country - - mv ./writable/uploads/user-agents* ./writable/uploads/user-agents + - mv ./writable/uploads/GeoLite2-City* ./writable/uploads/GeoLite2-City # create bundle folder: uses .rsync-filter (-F) file to copy only needed files - rsync -avF --progress . ./bundle diff --git a/DEPENDENCIES.md b/DEPENDENCIES.md index df0c8a6e8d518b0bbe6961fd4269b6a28d38f05a..1948629736dd154fbd2942e252557ebb382761c9 100644 --- a/DEPENDENCIES.md +++ b/DEPENDENCIES.md @@ -12,16 +12,20 @@ PHP Dependencies: - [commonmark](https://commonmark.thephpleague.com/) ([BSD 3-Clause "New" or "Revised" License](https://github.com/thephpleague/commonmark/blob/latest/LICENSE)) - [phpdotenv](https://github.com/vlucas/phpdotenv) ([ BSD-3-Clause License ](https://github.com/vlucas/phpdotenv/blob/master/LICENSE)) - [HTML To Markdown for PHP](https://github.com/thephpleague/html-to-markdown) ([MIT License](https://github.com/thephpleague/html-to-markdown/blob/master/LICENSE)) +- [podlibre/user-agents-php](https://github.com/podlibre/user-agents-php) ([MIT License](https://github.com/podlibre/user-agents-php/blob/main/LICENSE)) +- [podlibre/ipcat](https://github.com/podlibre/ipcat) ([GNU General Public License v3.0](https://github.com/podlibre/ipcat/blob/master/LICENSE)) Javascript dependencies: - [rollup](https://rollupjs.org/) ([MIT License](https://github.com/rollup/rollup/blob/master/LICENSE.md)) - [tailwindcss](https://tailwindcss.com/) ([MIT License](https://github.com/tailwindcss/tailwindcss/blob/master/LICENSE)) - [ProseMirror](https://prosemirror.net/) ([MIT License](https://github.com/ProseMirror/prosemirror/blob/master/LICENSE)) -- [D3: Data-Driven Documents](https://d3js.org) ([BSD 3-Clause "New" or "Revised" License](https://github.com/d3/d3/blob/master/LICENSE)) +- [amCharts 4](https://github.com/amcharts/amcharts4) ([Free amCharts license](https://github.com/amcharts/amcharts4/blob/master/dist/script/LICENSE)) - [Choices.js](https://joshuajohnson.co.uk/Choices/) ([MIT License](https://github.com/jshjohnson/Choices/blob/master/LICENSE)) Other: - [RemixIcon](https://remixicon.com/) ([Apache License 2.0](https://github.com/Remix-Design/RemixIcon/blob/master/License)) -- [User agent list](https://github.com/opawg/user-agents) ([by Open Podcast Analytics Working Group](https://github.com/opawg)) ([MIT license](https://github.com/opawg/user-agents/blob/master/LICENSE)) +- [OPAWG/User agent list](https://github.com/opawg/user-agents) ([by Open Podcast Analytics Working Group](https://github.com/opawg)) ([MIT license](https://github.com/opawg/user-agents/blob/master/LICENSE)) +- [client9/ipcat](https://github.com/client9/ipcat) ([GNU General Public License v3.0](https://github.com/client9/ipcat/blob/master/LICENSE)) +- [GeoLite2 City](https://dev.maxmind.com/geoip/geoip2/geolite2/) ([Attribution-ShareAlike 4.0 International (CC BY-SA 4.0)](https://www.maxmind.com/en/geolite2/eula)) diff --git a/app/Config/Routes.php b/app/Config/Routes.php index 3a786da094d2cb57fbda09887c7cb05e0a26d550..5d2c45f8051cdec4245b05133294d46209919b99 100644 --- a/app/Config/Routes.php +++ b/app/Config/Routes.php @@ -53,10 +53,14 @@ $routes->group(config('App')->installGateway, function ($routes) { ]); }); -// Route for podcast audio file analytics (/stats/podcast_id/episode_id/podcast_folder/filename.mp3) -$routes->add('stats/(:num)/(:num)/(:any)', 'Analytics::hit/$1/$2/$3', [ - 'as' => 'analytics_hit', -]); +// Route for podcast audio file analytics (/audio/podcast_id/episode_id/bytes_threshold/filesize/podcast_folder/filename.mp3) +$routes->add( + 'audio/(:num)/(:num)/(:num)/(:num)/(:any)', + 'Analytics::hit/$1/$2/$3/$4/$5', + [ + 'as' => 'analytics_hit', + ] +); // Show the Unknown UserAgents $routes->get('.well-known/unknown-useragents', 'UnknownUserAgents'); @@ -113,6 +117,26 @@ $routes->group( 'as' => 'podcast-delete', 'filter' => 'permission:podcasts-delete', ]); + $routes->get('analytics', 'Podcast::analytics/$1', [ + 'as' => 'podcast-analytics', + 'filter' => 'permission:podcasts-view,podcast-view', + ]); + $routes->get( + 'analytics-data/(:segment)/(:segment)', + 'AnalyticsData::getData/$1/$2/$3', + [ + 'as' => 'analytics-data', + 'filter' => 'permission:podcasts-view,podcast-view', + ] + ); + $routes->get( + 'analytics-data/(:segment)/(:segment)/(:num)', + 'AnalyticsData::getData/$1/$2/$3/$4', + [ + 'as' => 'analytics-filtered-data', + 'filter' => 'permission:podcasts-view,podcast-view', + ] + ); // Podcast episodes $routes->group('episodes', function ($routes) { diff --git a/app/Controllers/Admin/AnalyticsData.php b/app/Controllers/Admin/AnalyticsData.php new file mode 100644 index 0000000000000000000000000000000000000000..a57960dfd1561982dbdf33aab4d32bd2ba8657f3 --- /dev/null +++ b/app/Controllers/Admin/AnalyticsData.php @@ -0,0 +1,69 @@ +<?php + +/** + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Controllers\Admin; + +use App\Models\PodcastModel; +use App\Models\EpisodeModel; + +class AnalyticsData extends BaseController +{ + /** + * @var \App\Entities\Podcast|null + */ + protected $podcast; + protected $className; + protected $methodName; + protected $episode; + + public function _remap($method, ...$params) + { + if (count($params) > 2) { + if (!($this->podcast = (new PodcastModel())->find($params[0]))) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound( + 'Podcast not found: ' . $params[0] + ); + } + $this->className = '\App\Models\Analytics' . $params[1] . 'Model'; + $this->methodName = 'getData' . $params[2]; + if (count($params) > 3) { + if ( + !($this->episode = (new EpisodeModel()) + ->where([ + 'podcast_id' => $this->podcast->id, + 'id' => $params[3], + ]) + ->first()) + ) { + throw \CodeIgniter\Exceptions\PageNotFoundException::forPageNotFound( + 'Episode not found: ' . $params[3] + ); + } + } + } + + return $this->$method(); + } + public function getData() + { + $analytics_model = new $this->className(); + $methodName = $this->methodName; + if ($this->episode) { + return $this->response->setJSON( + $analytics_model->$methodName( + $this->podcast->id, + $this->episode->id + ) + ); + } else { + return $this->response->setJSON( + $analytics_model->$methodName($this->podcast->id) + ); + } + } +} diff --git a/app/Controllers/Admin/Podcast.php b/app/Controllers/Admin/Podcast.php index 439a86dafe359283d6483b8f7edfb33360cd5d2e..64594e07430fcf56196e43fe652a6bea67f2aaf4 100644 --- a/app/Controllers/Admin/Podcast.php +++ b/app/Controllers/Admin/Podcast.php @@ -58,6 +58,14 @@ class Podcast extends BaseController return view('admin/podcast/view', $data); } + public function analytics() + { + $data = ['podcast' => $this->podcast]; + + replace_breadcrumb_params([0 => $this->podcast->title]); + return view('admin/podcast/analytics', $data); + } + public function create() { helper(['form', 'misc']); @@ -204,7 +212,9 @@ class Podcast extends BaseController $podcast = new \App\Entities\Podcast([ 'name' => $this->request->getPost('name'), 'imported_feed_url' => $this->request->getPost('imported_feed_url'), - + 'new_feed_url' => base_url( + route_to('podcast_feed', $this->request->getPost('name')) + ), 'title' => $feed->channel[0]->title, 'description' => $feed->channel[0]->description, 'image' => download_file($nsItunes->image->attributes()), @@ -214,7 +224,9 @@ class Podcast extends BaseController ? null : (in_array($nsItunes->explicit, ['yes', 'true']) ? 'explicit' - : null), + : (in_array($nsItunes->explicit, ['no', 'false']) + ? 'clean' + : null)), 'owner_name' => $nsItunes->owner->name, 'owner_email' => $nsItunes->owner->email, 'publisher' => $nsItunes->author, @@ -302,11 +314,13 @@ class Podcast extends BaseController 'image' => empty($nsItunes->image->attributes()) ? null : download_file($nsItunes->image->attributes()), - 'explicit' => $nsItunes->explicit - ? (in_array($nsItunes->explicit, ['yes', 'true']) + 'parental_advisory' => empty($nsItunes->explicit) + ? null + : (in_array($nsItunes->explicit, ['yes', 'true']) ? 'explicit' - : null) - : null, + : (in_array($nsItunes->explicit, ['no', 'false']) + ? 'clean' + : null)), 'number' => $this->request->getPost('force_renumber') === 'yes' ? $itemNumber diff --git a/app/Controllers/Analytics.php b/app/Controllers/Analytics.php index ec89dc2260fed1fe3e8f901ef319fef9e101982f..3482b1eb6939fe694a05e32a1e81c09e5253c2d3 100644 --- a/app/Controllers/Analytics.php +++ b/app/Controllers/Analytics.php @@ -40,16 +40,22 @@ class Analytics extends Controller // E.g.: // $this->session = \Config\Services::session(); - set_user_session_country(); + set_user_session_deny_list_ip(); + set_user_session_location(); set_user_session_player(); } // Add one hit to this episode: - public function hit($p_podcastId, $p_episodeId, ...$filename) - { + public function hit( + $podcastId, + $episodeId, + $bytesThreshold, + $fileSize, + ...$filename + ) { helper('media'); - podcast_hit($p_podcastId, $p_episodeId); + podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize); return redirect()->to(media_url(implode('/', $filename))); } } diff --git a/app/Controllers/BaseController.php b/app/Controllers/BaseController.php index 2f5bdcff291b15a430e29ade0fdb7b390c6c3391..3bfbd4b0ffe5094dd49e53a9bee1afbff985845e 100644 --- a/app/Controllers/BaseController.php +++ b/app/Controllers/BaseController.php @@ -45,9 +45,10 @@ class BaseController extends Controller // E.g.: // $this->session = \Config\Services::session(); - set_user_session_country(); + set_user_session_deny_list_ip(); set_user_session_browser(); set_user_session_referer(); + set_user_session_entry_page(); } protected static function triggerWebpageHit($podcastId) diff --git a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php index a95e4db10869489e31b17f9e74b3bbc25a191a76..018315cfd952e78c4d6fcca01119c7b034501ae4 100644 --- a/app/Database/Migrations/2020-05-30-101500_add_podcasts.php +++ b/app/Database/Migrations/2020-05-30-101500_add_podcasts.php @@ -110,6 +110,13 @@ class AddPodcasts extends Migration 'The RSS feed URL if this podcast was imported, NULL otherwise.', 'null' => true, ], + 'new_feed_url' => [ + 'type' => 'VARCHAR', + 'constraint' => 1024, + 'comment' => + 'The RSS new feed URL if this podcast is moving out, NULL otherwise.', + 'null' => true, + ], 'created_at' => [ 'type' => 'TIMESTAMP', ], diff --git a/app/Database/Migrations/2020-06-05-170000_add_episodes.php b/app/Database/Migrations/2020-06-05-170000_add_episodes.php index 24b2f02fc5dd7497e9515abd1deb1813f41212a5..313346866b8c377c2004c18ed95f3621387afe6a 100644 --- a/app/Database/Migrations/2020-06-05-170000_add_episodes.php +++ b/app/Database/Migrations/2020-06-05-170000_add_episodes.php @@ -61,6 +61,12 @@ class AddEpisodes extends Migration 'unsigned' => true, 'comment' => 'File size in bytes', ], + 'enclosure_headersize' => [ + 'type' => 'INT', + 'constraint' => 10, + 'unsigned' => true, + 'comment' => 'Header size in bytes', + ], 'description' => [ 'type' => 'TEXT', 'null' => true, diff --git a/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php b/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php new file mode 100644 index 0000000000000000000000000000000000000000..e31d932d5f758bdd897334ffba807fea667b10d2 --- /dev/null +++ b/app/Database/Migrations/2020-06-08-120000_add_analytics_podcasts.php @@ -0,0 +1,49 @@ +<?php + +/** + * Class AddAnalyticsPodcastsByCountry + * Creates analytics_podcasts_by_country table in database + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Database\Migrations; + +use CodeIgniter\Database\Migration; + +class AddAnalyticsPodcasts extends Migration +{ + public function up() + { + $this->forge->addField([ + 'podcast_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + ], + 'date' => [ + 'type' => 'date', + ], + 'hits' => [ + 'type' => 'INT', + 'constraint' => 10, + 'default' => 1, + ], + ]); + $this->forge->addPrimaryKey(['podcast_id', 'date']); + $this->forge->addField( + '`created_at` timestamp NOT NULL DEFAULT current_timestamp()' + ); + $this->forge->addField( + '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()' + ); + $this->forge->addForeignKey('podcast_id', 'podcasts', 'id'); + $this->forge->createTable('analytics_podcasts'); + } + + public function down() + { + $this->forge->dropTable('analytics_podcasts'); + } +} diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_country.php b/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php similarity index 67% rename from app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_country.php rename to app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php index d0e94c6c9337b05830548bf4bf5a58679b7b178a..b189ef062b0a31325a910a9ef5c14230498b9e12 100644 --- a/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_country.php +++ b/app/Database/Migrations/2020-06-08-130000_add_analytics_podcasts_by_episode.php @@ -12,34 +12,28 @@ namespace App\Database\Migrations; use CodeIgniter\Database\Migration; -class AddAnalyticsEpisodesByCountry extends Migration +class AddAnalyticsPodcastsByEpisode extends Migration { public function up() { $this->forge->addField([ - 'id' => [ - 'type' => 'BIGINT', - 'constraint' => 20, - 'unsigned' => true, - 'auto_increment' => true, - ], 'podcast_id' => [ 'type' => 'BIGINT', 'constraint' => 20, 'unsigned' => true, ], + 'date' => [ + 'type' => 'date', + ], 'episode_id' => [ 'type' => 'BIGINT', 'constraint' => 20, 'unsigned' => true, ], - 'country_code' => [ - 'type' => 'VARCHAR', - 'constraint' => 3, - 'comment' => 'ISO 3166-1 code.', - ], - 'date' => [ - 'type' => 'date', + 'age' => [ + 'type' => 'INT', + 'constraint' => 10, + 'unsigned' => true, ], 'hits' => [ 'type' => 'INT', @@ -47,13 +41,7 @@ class AddAnalyticsEpisodesByCountry extends Migration 'default' => 1, ], ]); - $this->forge->addKey('id', true); - $this->forge->addUniqueKey([ - 'podcast_id', - 'episode_id', - 'country_code', - 'date', - ]); + $this->forge->addPrimaryKey(['podcast_id', 'episode_id', 'date']); $this->forge->addField( '`created_at` timestamp NOT NULL DEFAULT current_timestamp()' ); @@ -62,11 +50,11 @@ class AddAnalyticsEpisodesByCountry extends Migration ); $this->forge->addForeignKey('podcast_id', 'podcasts', 'id'); $this->forge->addForeignKey('episode_id', 'episodes', 'id'); - $this->forge->createTable('analytics_episodes_by_country'); + $this->forge->createTable('analytics_podcasts_by_episode'); } public function down() { - $this->forge->dropTable('analytics_episodes_by_country'); + $this->forge->dropTable('analytics_podcasts_by_episode'); } } diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_player.php b/app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php similarity index 70% rename from app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_player.php rename to app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php index 3a13f65fd59bba1ca950c73cc852e9d3d70b8dc0..c1bc04af881e3b5b57741ab80611bfbdf008fe1e 100644 --- a/app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_player.php +++ b/app/Database/Migrations/2020-06-08-140000_add_analytics_podcasts_by_player.php @@ -17,32 +17,45 @@ class AddAnalyticsPodcastsByPlayer extends Migration public function up() { $this->forge->addField([ - 'id' => [ - 'type' => 'BIGINT', - 'constraint' => 20, - 'unsigned' => true, - 'auto_increment' => true, - ], 'podcast_id' => [ 'type' => 'BIGINT', 'constraint' => 20, 'unsigned' => true, ], - 'player' => [ - 'type' => 'VARCHAR', - 'constraint' => 191, - ], 'date' => [ 'type' => 'date', ], + 'app' => [ + 'type' => 'VARCHAR', + 'constraint' => 128, + ], + 'device' => [ + 'type' => 'VARCHAR', + 'constraint' => 32, + ], + 'os' => [ + 'type' => 'VARCHAR', + 'constraint' => 32, + ], + 'bot' => [ + 'type' => 'TINYINT', + 'constraint' => 1, + 'default' => 0, + ], 'hits' => [ 'type' => 'INT', 'constraint' => 10, 'default' => 1, ], ]); - $this->forge->addKey('id', true); - $this->forge->addUniqueKey(['podcast_id', 'player', 'date']); + $this->forge->addPrimaryKey([ + 'podcast_id', + 'app', + 'device', + 'os', + 'bot', + 'date', + ]); $this->forge->addField( '`created_at` timestamp NOT NULL DEFAULT current_timestamp()' ); diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_country.php b/app/Database/Migrations/2020-06-08-150000_add_analytics_podcasts_by_country.php similarity index 83% rename from app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_country.php rename to app/Database/Migrations/2020-06-08-150000_add_analytics_podcasts_by_country.php index 6545a7a11bb1b81dd2b842323ad90357abdc6119..e5f045e6ef76ddf775c76b999e0ad1d0e1820ebd 100644 --- a/app/Database/Migrations/2020-06-08-210000_add_analytics_podcasts_by_country.php +++ b/app/Database/Migrations/2020-06-08-150000_add_analytics_podcasts_by_country.php @@ -17,33 +17,26 @@ class AddAnalyticsPodcastsByCountry extends Migration public function up() { $this->forge->addField([ - 'id' => [ - 'type' => 'BIGINT', - 'constraint' => 20, - 'unsigned' => true, - 'auto_increment' => true, - ], 'podcast_id' => [ 'type' => 'BIGINT', 'constraint' => 20, 'unsigned' => true, ], + 'date' => [ + 'type' => 'date', + ], 'country_code' => [ 'type' => 'VARCHAR', 'constraint' => 3, 'comment' => 'ISO 3166-1 code.', ], - 'date' => [ - 'type' => 'date', - ], 'hits' => [ 'type' => 'INT', 'constraint' => 10, 'default' => 1, ], ]); - $this->forge->addKey('id', true); - $this->forge->addUniqueKey(['podcast_id', 'country_code', 'date']); + $this->forge->addPrimaryKey(['podcast_id', 'country_code', 'date']); $this->forge->addField( '`created_at` timestamp NOT NULL DEFAULT current_timestamp()' ); diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_country.php b/app/Database/Migrations/2020-06-08-160000_add_analytics_podcasts_by_region.php similarity index 60% rename from app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_country.php rename to app/Database/Migrations/2020-06-08-160000_add_analytics_podcasts_by_region.php index 7f8b1415c0fd0f0afb1242249ae2b1b50790c074..7b787878deec93505bd454c2eab19270845329a4 100644 --- a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_country.php +++ b/app/Database/Migrations/2020-06-08-160000_add_analytics_podcasts_by_region.php @@ -1,8 +1,8 @@ <?php /** - * Class AddAnalyticsWebsiteByCountry - * Creates analytics_website_by_country table in database + * Class AddAnalyticsPodcastsByRegion + * Creates analytics_podcasts_by_region table in database * @copyright 2020 Podlibre * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ @@ -12,29 +12,36 @@ namespace App\Database\Migrations; use CodeIgniter\Database\Migration; -class AddAnalyticsWebsiteByCountry extends Migration +class AddAnalyticsPodcastsByRegion extends Migration { public function up() { $this->forge->addField([ - 'id' => [ - 'type' => 'BIGINT', - 'constraint' => 20, - 'unsigned' => true, - 'auto_increment' => true, - ], 'podcast_id' => [ 'type' => 'BIGINT', 'constraint' => 20, 'unsigned' => true, ], + 'date' => [ + 'type' => 'date', + ], 'country_code' => [ 'type' => 'VARCHAR', 'constraint' => 3, 'comment' => 'ISO 3166-1 code.', ], - 'date' => [ - 'type' => 'date', + 'region_code' => [ + 'type' => 'VARCHAR', + 'constraint' => 3, + 'comment' => 'ISO 3166-2 code.', + ], + 'latitude' => [ + 'type' => 'FLOAT', + 'null' => true, + ], + 'longitude' => [ + 'type' => 'FLOAT', + 'null' => true, ], 'hits' => [ 'type' => 'INT', @@ -42,8 +49,12 @@ class AddAnalyticsWebsiteByCountry extends Migration 'default' => 1, ], ]); - $this->forge->addKey('id', true); - $this->forge->addUniqueKey(['podcast_id', 'country_code', 'date']); + $this->forge->addPrimaryKey([ + 'podcast_id', + 'country_code', + 'region_code', + 'date', + ]); $this->forge->addField( '`created_at` timestamp NOT NULL DEFAULT current_timestamp()' ); @@ -51,11 +62,11 @@ class AddAnalyticsWebsiteByCountry extends Migration '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()' ); $this->forge->addForeignKey('podcast_id', 'podcasts', 'id'); - $this->forge->createTable('analytics_website_by_country'); + $this->forge->createTable('analytics_podcasts_by_region'); } public function down() { - $this->forge->dropTable('analytics_website_by_country'); + $this->forge->dropTable('analytics_podcasts_by_region'); } } diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_browser.php b/app/Database/Migrations/2020-06-08-170000_add_analytics_website_by_browser.php similarity index 82% rename from app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_browser.php rename to app/Database/Migrations/2020-06-08-170000_add_analytics_website_by_browser.php index 6e4942d43317c4f7a70577ae4ab345fb10401cf8..21724af7097c7025a23b9aac381437ca2bf751bd 100644 --- a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_browser.php +++ b/app/Database/Migrations/2020-06-08-170000_add_analytics_website_by_browser.php @@ -17,32 +17,26 @@ class AddAnalyticsWebsiteByBrowser extends Migration public function up() { $this->forge->addField([ - 'id' => [ - 'type' => 'BIGINT', - 'constraint' => 20, - 'unsigned' => true, - 'auto_increment' => true, - ], 'podcast_id' => [ 'type' => 'BIGINT', 'constraint' => 20, 'unsigned' => true, ], + 'date' => [ + 'type' => 'date', + ], 'browser' => [ 'type' => 'VARCHAR', 'constraint' => 191, ], - 'date' => [ - 'type' => 'date', - ], + 'hits' => [ 'type' => 'INT', 'constraint' => 10, 'default' => 1, ], ]); - $this->forge->addKey('id', true); - $this->forge->addUniqueKey(['podcast_id', 'browser', 'date']); + $this->forge->addPrimaryKey(['podcast_id', 'browser', 'date']); $this->forge->addField( '`created_at` timestamp NOT NULL DEFAULT current_timestamp()' ); diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_referer.php b/app/Database/Migrations/2020-06-08-180000_add_analytics_website_by_referer.php similarity index 78% rename from app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_referer.php rename to app/Database/Migrations/2020-06-08-180000_add_analytics_website_by_referer.php index 28808f273e40adf36d617f2ead748070083df75f..579024b02b845edfbf88ab79eaadb9ca8dd11e3c 100644 --- a/app/Database/Migrations/2020-06-08-210000_add_analytics_website_by_referer.php +++ b/app/Database/Migrations/2020-06-08-180000_add_analytics_website_by_referer.php @@ -17,24 +17,28 @@ class AddAnalyticsWebsiteByReferer extends Migration public function up() { $this->forge->addField([ - 'id' => [ - 'type' => 'BIGINT', - 'constraint' => 20, - 'unsigned' => true, - 'auto_increment' => true, - ], 'podcast_id' => [ 'type' => 'BIGINT', 'constraint' => 20, 'unsigned' => true, ], + 'date' => [ + 'type' => 'date', + ], 'referer' => [ 'type' => 'VARCHAR', - 'constraint' => 191, + 'constraint' => 512, 'comment' => 'Referer URL.', ], - 'date' => [ - 'type' => 'date', + 'domain' => [ + 'type' => 'VARCHAR', + 'constraint' => 128, + 'null' => true, + ], + 'keywords' => [ + 'type' => 'VARCHAR', + 'constraint' => 384, + 'null' => true, ], 'hits' => [ 'type' => 'INT', @@ -42,8 +46,7 @@ class AddAnalyticsWebsiteByReferer extends Migration 'default' => 1, ], ]); - $this->forge->addKey('id', true); - $this->forge->addUniqueKey(['podcast_id', 'referer', 'date']); + $this->forge->addPrimaryKey(['podcast_id', 'referer', 'date']); $this->forge->addField( '`created_at` timestamp NOT NULL DEFAULT current_timestamp()' ); diff --git a/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php b/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php new file mode 100644 index 0000000000000000000000000000000000000000..19bce6de19fb00b85be29b074b5899cc6518b5a3 --- /dev/null +++ b/app/Database/Migrations/2020-06-08-190000_add_analytics_website_by_entry_page.php @@ -0,0 +1,54 @@ +<?php + +/** + * Class AddAnalyticsWebsiteByReferer + * Creates analytics_website_by_referer table in database + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Database\Migrations; + +use CodeIgniter\Database\Migration; + +class AddAnalyticsWebsiteByEntryPage extends Migration +{ + public function up() + { + $this->forge->addField([ + 'podcast_id' => [ + 'type' => 'BIGINT', + 'constraint' => 20, + 'unsigned' => true, + ], + 'date' => [ + 'type' => 'date', + ], + 'entry_page' => [ + 'type' => 'VARCHAR', + 'constraint' => 512, + 'comment' => 'Entry page URL.', + ], + 'hits' => [ + 'type' => 'INT', + 'constraint' => 10, + 'default' => 1, + ], + ]); + $this->forge->addPrimaryKey(['podcast_id', 'entry_page', 'date']); + $this->forge->addField( + '`created_at` timestamp NOT NULL DEFAULT current_timestamp()' + ); + $this->forge->addField( + '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()' + ); + $this->forge->addForeignKey('podcast_id', 'podcasts', 'id'); + $this->forge->createTable('analytics_website_by_entry_page'); + } + + public function down() + { + $this->forge->dropTable('analytics_website_by_entry_page'); + } +} diff --git a/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_player.php b/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_player.php deleted file mode 100644 index 3a1e257af60fd0f47cb6e814ca0bd3b272dca3e0..0000000000000000000000000000000000000000 --- a/app/Database/Migrations/2020-06-08-210000_add_analytics_episodes_by_player.php +++ /dev/null @@ -1,71 +0,0 @@ -<?php - -/** - * Class AddAnalyticsEpisodesByPlayer - * Creates analytics_episodes_by_player table in database - * @copyright 2020 Podlibre - * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 - * @link https://castopod.org/ - */ - -namespace App\Database\Migrations; - -use CodeIgniter\Database\Migration; - -class AddAnalyticsEpisodesByPlayer extends Migration -{ - public function up() - { - $this->forge->addField([ - 'id' => [ - 'type' => 'BIGINT', - 'constraint' => 20, - 'unsigned' => true, - 'auto_increment' => true, - ], - 'podcast_id' => [ - 'type' => 'BIGINT', - 'constraint' => 20, - 'unsigned' => true, - ], - 'episode_id' => [ - 'type' => 'BIGINT', - 'constraint' => 20, - 'unsigned' => true, - ], - 'player' => [ - 'type' => 'VARCHAR', - 'constraint' => 191, - ], - 'date' => [ - 'type' => 'date', - ], - 'hits' => [ - 'type' => 'INT', - 'constraint' => 10, - 'default' => 1, - ], - ]); - $this->forge->addKey('id', true); - $this->forge->addUniqueKey([ - 'podcast_id', - 'episode_id', - 'player', - 'date', - ]); - $this->forge->addField( - '`created_at` timestamp NOT NULL DEFAULT current_timestamp()' - ); - $this->forge->addField( - '`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp()' - ); - $this->forge->addForeignKey('podcast_id', 'podcasts', 'id'); - $this->forge->addForeignKey('episode_id', 'episodes', 'id'); - $this->forge->createTable('analytics_episodes_by_player'); - } - - public function down() - { - $this->forge->dropTable('analytics_episodes_by_player'); - } -} diff --git a/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php b/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php index 18bd203af422ba127b5270491b3b93e96a99da51..caf355009a0420b2581c9cab9439c9737bb29642 100644 --- a/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php +++ b/app/Database/Migrations/2020-06-11-210000_add_analytics_podcasts_stored_procedure.php @@ -18,26 +18,42 @@ class AddAnalyticsPodcastsStoredProcedure extends Migration { // Creates Stored Procedure for data insertion // Example: CALL analytics_podcasts(1,2,'FR','phone/android/Deezer'); - $procedureName = $this->db->prefixTable('analytics_podcasts'); - $episodesTableName = $this->db->prefixTable('analytics_episodes'); + $prefix = $this->db->getPrefix(); + $createQuery = <<<EOD -CREATE PROCEDURE `$procedureName` (IN `p_podcast_id` BIGINT(20) UNSIGNED, IN `p_episode_id` BIGINT(20) UNSIGNED, IN `p_country_code` VARCHAR(3) CHARSET utf8mb4, IN `p_player` VARCHAR(191) CHARSET utf8mb4) MODIFIES SQL DATA +CREATE PROCEDURE `{$prefix}analytics_podcasts` ( + IN `p_podcast_id` BIGINT(20) UNSIGNED, + IN `p_episode_id` BIGINT(20) UNSIGNED, + IN `p_country_code` VARCHAR(3) CHARSET utf8mb4, + IN `p_region_code` VARCHAR(3) CHARSET utf8mb4, + IN `p_latitude` FLOAT, + IN `p_longitude` FLOAT, + IN `p_app` VARCHAR(128) CHARSET utf8mb4, + IN `p_device` VARCHAR(32) CHARSET utf8mb4, + IN `p_os` VARCHAR(32) CHARSET utf8mb4, + IN `p_bot` TINYINT(1) UNSIGNED + ) MODIFIES SQL DATA DETERMINISTIC SQL SECURITY INVOKER COMMENT 'Add one hit in podcast logs tables.' BEGIN -INSERT INTO `{$procedureName}_by_country`(`podcast_id`, `country_code`, `date`) -VALUES (p_podcast_id, p_country_code, DATE(NOW())) -ON DUPLICATE KEY UPDATE `hits`=`hits`+1; -INSERT INTO `{$procedureName}_by_player`(`podcast_id`, `player`, `date`) -VALUES (p_podcast_id, p_player, DATE(NOW())) -ON DUPLICATE KEY UPDATE `hits`=`hits`+1; -INSERT INTO `{$episodesTableName}_by_country`(`podcast_id`, `episode_id`, `country_code`, `date`) -VALUES (p_podcast_id, p_episode_id, p_country_code, DATE(NOW())) -ON DUPLICATE KEY UPDATE `hits`=`hits`+1; -INSERT INTO `{$episodesTableName}_by_player`(`podcast_id`, `episode_id`, `player`, `date`) -VALUES (p_podcast_id, p_episode_id, p_player, DATE(NOW())) -ON DUPLICATE KEY UPDATE `hits`=`hits`+1; +IF NOT `p_bot` THEN + INSERT INTO `{$prefix}analytics_podcasts`(`podcast_id`, `date`) + VALUES (p_podcast_id, DATE(NOW())) + ON DUPLICATE KEY UPDATE `hits`=`hits`+1; + INSERT INTO `{$prefix}analytics_podcasts_by_episode`(`podcast_id`, `episode_id`, `date`, `age`) + SELECT p_podcast_id, p_episode_id, DATE(NOW()), datediff(now(),`published_at`) FROM `{$prefix}episodes` WHERE `id`= p_episode_id + ON DUPLICATE KEY UPDATE `hits`=`hits`+1; + INSERT INTO `{$prefix}analytics_podcasts_by_country`(`podcast_id`, `country_code`, `date`) + VALUES (p_podcast_id, p_country_code, DATE(NOW())) + ON DUPLICATE KEY UPDATE `hits`=`hits`+1; + INSERT INTO `{$prefix}analytics_podcasts_by_region`(`podcast_id`, `country_code`, `region_code`, `latitude`, `longitude`, `date`) + VALUES (p_podcast_id, p_country_code, p_region_code, p_latitude, p_longitude, DATE(NOW())) + ON DUPLICATE KEY UPDATE `hits`=`hits`+1; +END IF; +INSERT INTO `{$prefix}analytics_podcasts_by_player`(`podcast_id`, `app`, `device`, `os`, `bot`, `date`) + VALUES (p_podcast_id, p_app, p_device, p_os, p_bot, DATE(NOW())) + ON DUPLICATE KEY UPDATE `hits`=`hits`+1; END EOD; $this->db->query($createQuery); @@ -45,7 +61,9 @@ EOD; public function down() { - $procedureName = $this->db->prefixTable('analytics_podcasts'); - $this->db->query("DROP PROCEDURE IF EXISTS `$procedureName`"); + $prefix = $this->db->getPrefix(); + $this->db->query( + "DROP PROCEDURE IF EXISTS `{$prefix}analytics_podcasts`" + ); } } diff --git a/app/Database/Migrations/2020-06-11-210000_add_analytics_website_stored_procedure.php b/app/Database/Migrations/2020-06-11-210000_add_analytics_website_stored_procedure.php index c263a8a1b3d79b83f4c4cc45298e60d9c7a61a7f..836b5d7d37612f07d1b773cc25b8f9242c2fa053 100644 --- a/app/Database/Migrations/2020-06-11-210000_add_analytics_website_stored_procedure.php +++ b/app/Database/Migrations/2020-06-11-210000_add_analytics_website_stored_procedure.php @@ -20,20 +20,20 @@ class AddAnalyticsWebsiteStoredProcedure extends Migration // Example: CALL analytics_website(1,'FR','Firefox'); $procedureName = $this->db->prefixTable('analytics_website'); $createQuery = <<<EOD -CREATE PROCEDURE `$procedureName` (IN `p_podcast_id` BIGINT(20) UNSIGNED, IN `p_country_code` VARCHAR(3) CHARSET utf8mb4, IN `p_browser` VARCHAR(191) CHARSET utf8mb4, IN `p_referer` VARCHAR(191) CHARSET utf8mb4) MODIFIES SQL DATA +CREATE PROCEDURE `$procedureName` (IN `p_podcast_id` BIGINT(20) UNSIGNED, IN `p_browser` VARCHAR(191) CHARSET utf8mb4, IN `p_entry_page` VARCHAR(512) CHARSET utf8mb4, IN `p_referer` VARCHAR(512) CHARSET utf8mb4, IN `p_domain` VARCHAR(128) CHARSET utf8mb4, IN `p_keywords` VARCHAR(384) CHARSET utf8mb4) MODIFIES SQL DATA DETERMINISTIC SQL SECURITY INVOKER COMMENT 'Add one hit in website logs tables.' BEGIN -INSERT INTO {$procedureName}_by_country(`podcast_id`, `country_code`, `date`) -VALUES (p_podcast_id, p_country_code, DATE(NOW())) -ON DUPLICATE KEY UPDATE `hits`=`hits`+1; INSERT INTO {$procedureName}_by_browser(`podcast_id`, `browser`, `date`) -VALUES (p_podcast_id, p_browser, DATE(NOW())) -ON DUPLICATE KEY UPDATE `hits`=`hits`+1; -INSERT INTO {$procedureName}_by_referer(`podcast_id`, `referer`, `date`) -VALUES (p_podcast_id, p_referer, DATE(NOW())) -ON DUPLICATE KEY UPDATE `hits`=`hits`+1; + VALUES (p_podcast_id, p_browser, DATE(NOW())) + ON DUPLICATE KEY UPDATE `hits`=`hits`+1; +INSERT INTO {$procedureName}_by_referer(`podcast_id`, `referer`, `domain`, `keywords`, `date`) + VALUES (p_podcast_id, p_referer, p_domain, p_keywords, DATE(NOW())) + ON DUPLICATE KEY UPDATE `hits`=`hits`+1; +INSERT INTO {$procedureName}_by_entry_page(`podcast_id`, `entry_page`, `date`) + VALUES (p_podcast_id, p_entry_page, DATE(NOW())) + ON DUPLICATE KEY UPDATE `hits`=`hits`+1; END EOD; $this->db->query($createQuery); diff --git a/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php b/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php new file mode 100644 index 0000000000000000000000000000000000000000..e312a3a0160e6c8838afcc2e529d00d99c5984a4 --- /dev/null +++ b/app/Database/Seeds/FakePodcastsAnalyticsSeeder.php @@ -0,0 +1,176 @@ +<?php + +/** + * Class FakePodcastsAnalyticsSeeder + * Inserts Fake Analytics in the database + * + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Database\Seeds; +use App\Models\PodcastModel; +use App\Models\EpisodeModel; + +use CodeIgniter\Database\Seeder; + +class FakePodcastsAnalyticsSeeder extends Seeder +{ + public function run() + { + $podcast = (new PodcastModel())->first(); + + $jsonUserAgents = json_decode( + file_get_contents( + 'https://raw.githubusercontent.com/opawg/user-agents/master/src/user-agents.json' + ), + true + ); + + if ($podcast) { + $firstEpisode = (new EpisodeModel()) + ->selectMin('published_at') + ->first(); + + for ( + $date = strtotime($firstEpisode->published_at); + $date < strtotime('now'); + $date = strtotime(date('Y-m-d', $date) . ' +1 day') + ) { + $analytics_podcasts = []; + $analytics_podcasts_by_country = []; + $analytics_podcasts_by_episode = []; + $analytics_podcasts_by_player = []; + $analytics_podcasts_by_region = []; + + $episodes = (new EpisodeModel()) + ->where([ + 'podcast_id' => $podcast->id, + 'DATE(published_at) <=' => date('Y-m-d', $date), + ]) + ->findAll(); + foreach ($episodes as $episode) { + $age = floor( + ($date - strtotime($episode->published_at)) / 86400 + ); + $proba1 = floor(exp(3 - $age / 40)) + 1; + + for ( + $num_line = 0; + $num_line < rand(1, $proba1); + $num_line++ + ) { + $proba2 = floor(exp(6 - $age / 20)) + 10; + + $player = + $jsonUserAgents[ + rand(1, count($jsonUserAgents) - 1) + ]; + $app = isset($player['app']) ? $player['app'] : ''; + $device = isset($player['device']) + ? $player['device'] + : ''; + $os = isset($player['os']) ? $player['os'] : ''; + $bot = isset($player['bot']) ? $player['bot'] : 0; + + $fakeIp = + rand(0, 255) . + '.' . + rand(0, 255) . + '.' . + rand(0, 255) . + '.' . + rand(0, 255); + + $cityReader = new \GeoIp2\Database\Reader( + WRITEPATH . + 'uploads/GeoLite2-City/GeoLite2-City.mmdb' + ); + + $countryCode = 'N/A'; + $regionCode = 'N/A'; + $latitude = null; + $longitude = null; + try { + $city = $cityReader->city($fakeIp); + + $countryCode = empty($city->country->isoCode) + ? 'N/A' + : $city->country->isoCode; + + $regionCode = empty($city->subdivisions[0]->isoCode) + ? 'N/A' + : $city->subdivisions[0]->isoCode; + $latitude = round($city->location->latitude, 3); + $longitude = round($city->location->longitude, 3); + } catch (\GeoIp2\Exception\AddressNotFoundException $ex) { + //Bad luck, bad IP, nothing to do. + } + + $hits = rand(0, $proba2); + + $analytics_podcasts[] = [ + 'podcast_id' => $podcast->id, + 'date' => date('Y-m-d', $date), + 'hits' => $hits, + ]; + $analytics_podcasts_by_country[] = [ + 'podcast_id' => $podcast->id, + 'date' => date('Y-m-d', $date), + 'country_code' => $countryCode, + 'hits' => $hits, + ]; + $analytics_podcasts_by_episode[] = [ + 'podcast_id' => $podcast->id, + 'date' => date('Y-m-d', $date), + 'episode_id' => $episode->id, + 'age' => $age, + 'hits' => $hits, + ]; + $analytics_podcasts_by_player[] = [ + 'podcast_id' => $podcast->id, + 'date' => date('Y-m-d', $date), + 'app' => $app, + 'device' => $device, + 'os' => $os, + 'bot' => $bot, + 'hits' => $hits, + ]; + $analytics_podcasts_by_region[] = [ + 'podcast_id' => $podcast->id, + 'date' => date('Y-m-d', $date), + 'country_code' => $countryCode, + 'region_code' => $regionCode, + 'latitude' => $latitude, + 'longitude' => $longitude, + 'hits' => $hits, + ]; + } + } + $this->db + ->table('analytics_podcasts') + ->ignore(true) + ->insertBatch($analytics_podcasts); + $this->db + ->table('analytics_podcasts_by_country') + ->ignore(true) + ->insertBatch($analytics_podcasts_by_country); + $this->db + ->table('analytics_podcasts_by_episode') + ->ignore(true) + ->insertBatch($analytics_podcasts_by_episode); + $this->db + ->table('analytics_podcasts_by_player') + ->ignore(true) + ->insertBatch($analytics_podcasts_by_player); + $this->db + ->table('analytics_podcasts_by_region') + ->ignore(true) + ->insertBatch($analytics_podcasts_by_region); + } + } else { + echo "Create one podcast and some episodes first.\n"; + } + } +} diff --git a/app/Database/Seeds/FakeWebsiteAnalyticsSeeder.php b/app/Database/Seeds/FakeWebsiteAnalyticsSeeder.php new file mode 100644 index 0000000000000000000000000000000000000000..67270d4c5ef5e53cbe5d9953ce0a93461c25af1f --- /dev/null +++ b/app/Database/Seeds/FakeWebsiteAnalyticsSeeder.php @@ -0,0 +1,260 @@ +<?php + +/** + * Class FakeWebsiteAnalyticsSeeder + * Inserts Fake Analytics in the database + * + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Database\Seeds; +use App\Models\PodcastModel; +use App\Models\EpisodeModel; + +use CodeIgniter\Database\Seeder; + +class FakeWebsiteAnalyticsSeeder extends Seeder +{ + protected $keywords = [ + 'all the smoke podcast', + 'apple podcast', + 'bad friends podcast', + 'best podcast', + 'best podcasts', + 'best podcasts 2020', + 'blood ties', + 'call her daddy', + 'call her daddy podcast', + 'call her daddy podcast controversy', + 'call her daddy podcast drama', + 'counter clock podcast', + 'counterclock podcast', + 'crime junkie podcast', + 'crime podcast', + 'down the hill podcast', + 'gerry callahan podcast', + 'google podcast', + 'history podcast', + 'joe rogan', + 'joe rogan podcast', + 'lana rhoades and logan paul podcast', + 'last podcast on the left', + 'michael moore podcast', + 'michelle obama podcast', + 'missing in alaska podcast', + 'murder podcast', + 'nice white parents podcast', + 'nick cannon podcast', + 'npr podcast', + 'office ladies podcast', + 'podcast app', + 'podcasts', + 'rogan podcast', + 'rudy giuliani podcast', + 'savage podcast', + 'serial podcast', + 'smartless podcast', + 'ted cruz podcast', + 'the daily', + 'the daily podcast', + 'the last podcast on the left', + 'the new abnormal podcast', + 'tiger king podcast', + 'trey gowdy podcast', + 'true crime podcast', + 'what is a podcast', + 'what is podcast', + 'wind of change podcast', + 'your own backyard podcast', + ]; + protected $domains = [ + '360.cn ', + 'adobe.com ', + 'aliexpress.com ', + 'alipay.com ', + 'amazon.co.jp ', + 'amazon.com ', + 'amazon.in ', + 'apple.com ', + 'baidu.com ', + 'bing.com ', + 'bongacams.com ', + 'chaturbate.com ', + 'china.com.cn ', + 'csdn.net ', + 'ebay.com ', + 'facebook.com ', + 'google.co.in ', + 'google.com ', + 'google.com.hk ', + 'instagram.com ', + 'jd.com ', + 'live.com ', + 'livejasmin.com ', + 'microsoft.com ', + 'microsoftonline.com ', + 'myshopify.com ', + 'naver.com ', + 'netflix.com ', + 'office.com ', + 'okezone.com ', + 'panda.tv ', + 'qq.com ', + 'reddit.com ', + 'sina.com.cn ', + 'sohu.com ', + 'taobao.com ', + 'tianya.cn ', + 'tmall.com ', + 'tribunnews.com ', + 'twitch.tv ', + 'twitter.com ', + 'vk.com ', + 'weibo.com ', + 'wikipedia.org ', + 'xinhuanet.com ', + 'yahoo.co.jp ', + 'yahoo.com ', + 'youtube.com ', + 'zhanqi.tv ', + 'zoom.us ', + ]; + + protected $browsers = [ + 'Android Browser', + 'Avast Secure Browser', + 'BlackBerry Browser', + 'Chrome', + 'Chrome Mobile', + 'Chrome Mobile iOS', + 'Chrome Webview', + 'Chromium', + 'Ecosia', + 'Fennec', + 'Firebird', + 'Firefox', + 'Firefox Mobile', + 'Firefox Mobile iOS', + 'Galeon', + 'GNOME Web', + 'Headless Chrome', + 'Huawei Browser', + 'IE Mobile', + 'Inconnu', + 'Internet Explorer', + 'Kindle Browser', + 'Konqueror', + 'Maxthon', + 'Meizu Browser', + 'Microsoft Edge', + 'MIUI Browser', + 'Mobile Safari', + 'Mobile Silk', + 'OmniWeb', + 'Openwave Mobile Browser', + 'Opera', + 'Opera Mini', + 'Opera Mobile', + 'Opera Next', + 'Palm Blazer', + 'Puffin', + 'QupZilla', + 'Safari', + 'Samsung Browser', + 'UC Browser', + 'WOSBrowser', + ]; + + public function run() + { + $podcast = (new PodcastModel())->first(); + + if ($podcast) { + $firstEpisode = (new EpisodeModel()) + ->selectMin('published_at') + ->first(); + + for ( + $date = strtotime($firstEpisode->published_at); + $date < strtotime('now'); + $date = strtotime(date('Y-m-d', $date) . ' +1 day') + ) { + $website_by_browser = []; + $website_by_entry_page = []; + $website_by_referer = []; + + $episodes = (new EpisodeModel()) + ->where([ + 'podcast_id' => $podcast->id, + 'DATE(published_at) <=' => date('Y-m-d', $date), + ]) + ->findAll(); + foreach ($episodes as $episode) { + $age = floor( + ($date - strtotime($episode->published_at)) / 86400 + ); + $proba1 = floor(exp(3 - $age / 40)) + 1; + + for ( + $num_line = 0; + $num_line < rand(1, $proba1); + $num_line++ + ) { + $proba2 = floor(exp(6 - $age / 20)) + 10; + + $domain = + $this->domains[rand(0, count($this->domains) - 1)]; + $keyword = + $this->keywords[ + rand(0, count($this->keywords) - 1) + ]; + $browser = + $this->browsers[ + rand(0, count($this->browsers) - 1) + ]; + + $hits = rand(0, $proba2); + + $website_by_browser[] = [ + 'podcast_id' => $podcast->id, + 'date' => date('Y-m-d', $date), + 'browser' => $browser, + 'hits' => $hits, + ]; + $website_by_entry_page[] = [ + 'podcast_id' => $podcast->id, + 'date' => date('Y-m-d', $date), + 'entry_page' => $episode->link, + 'hits' => $hits, + ]; + $website_by_referer[] = [ + 'podcast_id' => $podcast->id, + 'date' => date('Y-m-d', $date), + 'referer' => + 'http://' . $domain . '/?q=' . $keyword, + 'domain' => $domain, + 'keywords' => $keyword, + 'hits' => $hits, + ]; + } + } + $this->db + ->table('analytics_website_by_browser') + ->ignore(true) + ->insertBatch($website_by_browser); + $this->db + ->table('analytics_website_by_entry_page') + ->ignore(true) + ->insertBatch($website_by_entry_page); + $this->db + ->table('analytics_website_by_referer') + ->ignore(true) + ->insertBatch($website_by_referer); + } + } else { + echo "Create one podcast and some episodes first.\n"; + } + } +} diff --git a/app/Entities/AnalyticsWebsiteByCountry.php b/app/Entities/AnalyticsPodcasts.php similarity index 67% rename from app/Entities/AnalyticsWebsiteByCountry.php rename to app/Entities/AnalyticsPodcasts.php index 9839e3cb71f0e6db47b33368707a9614ddec5910..7f0f169e17cc6a33da15a39890de8b373ccf764e 100644 --- a/app/Entities/AnalyticsWebsiteByCountry.php +++ b/app/Entities/AnalyticsPodcasts.php @@ -1,8 +1,8 @@ <?php /** - * Class AnalyticsWebsiteByCountry - * Entity for AnalyticsWebsiteByCountry + * Class AnalyticsPodcasts + * Entity for AnalyticsPodcasts * @copyright 2020 Podlibre * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ @@ -12,11 +12,10 @@ namespace App\Entities; use CodeIgniter\Entity; -class AnalyticsWebsiteByCountry extends Entity +class AnalyticsPodcasts extends Entity { protected $casts = [ 'podcast_id' => 'integer', - 'country_code' => 'string', 'date' => 'datetime', 'hits' => 'integer', ]; diff --git a/app/Entities/AnalyticsEpisodesByPlayer.php b/app/Entities/AnalyticsPodcastsByEpisode.php similarity index 70% rename from app/Entities/AnalyticsEpisodesByPlayer.php rename to app/Entities/AnalyticsPodcastsByEpisode.php index 3e48c0aa95711b452e13d416e7e9992eade15b29..783bf2d54e1c13f2522d5c8ee99786e9bc570689 100644 --- a/app/Entities/AnalyticsEpisodesByPlayer.php +++ b/app/Entities/AnalyticsPodcastsByEpisode.php @@ -1,8 +1,8 @@ <?php /** - * Class AnalyticsEpisodesByPlayer - * Entity for AnalyticsEpisodesByPlayer + * Class AnalyticsPodcastsByEpisode + * Entity for AnalyticsPodcastsByEpisode * @copyright 2020 Podlibre * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ @@ -12,12 +12,11 @@ namespace App\Entities; use CodeIgniter\Entity; -class AnalyticsEpisodesByPlayer extends Entity +class AnalyticsPodcastsByEpisode extends Entity { protected $casts = [ 'podcast_id' => 'integer', 'episode_id' => 'integer', - 'player' => 'string', 'date' => 'datetime', 'hits' => 'integer', ]; diff --git a/app/Entities/AnalyticsPodcastsByPlayer.php b/app/Entities/AnalyticsPodcastsByPlayer.php index 9e33ba980704e67fd6c72354431284e91cbdf737..b0e19d26948fe5017458651d2eb0efdf6f118456 100644 --- a/app/Entities/AnalyticsPodcastsByPlayer.php +++ b/app/Entities/AnalyticsPodcastsByPlayer.php @@ -16,7 +16,10 @@ class AnalyticsPodcastsByPlayer extends Entity { protected $casts = [ 'podcast_id' => 'integer', - 'player' => 'string', + 'app' => '?string', + 'device' => '?string', + 'os' => '?string', + 'bot' => 'boolean', 'date' => 'datetime', 'hits' => 'integer', ]; diff --git a/app/Entities/AnalyticsPodcastsByRegion.php b/app/Entities/AnalyticsPodcastsByRegion.php new file mode 100644 index 0000000000000000000000000000000000000000..8f6a9d603056f4c5d697e3569c0b3337de031e98 --- /dev/null +++ b/app/Entities/AnalyticsPodcastsByRegion.php @@ -0,0 +1,26 @@ +<?php + +/** + * Class AnalyticsPodcastsByRegion + * Entity for AnalyticsPodcastsByRegion + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Entities; + +use CodeIgniter\Entity; + +class AnalyticsPodcastsByRegion extends Entity +{ + protected $casts = [ + 'podcast_id' => 'integer', + 'country_code' => 'string', + 'region_code' => '?string', + 'latitude' => '?float', + 'longitude' => '?float', + 'date' => 'datetime', + 'hits' => 'integer', + ]; +} diff --git a/app/Entities/AnalyticsEpisodesByCountry.php b/app/Entities/AnalyticsWebsiteByEntryPage.php similarity index 62% rename from app/Entities/AnalyticsEpisodesByCountry.php rename to app/Entities/AnalyticsWebsiteByEntryPage.php index b1736443194e8967ff958caa4d778f596069a46c..344d60fb2bc4dd136977c656d84b715a015c5a96 100644 --- a/app/Entities/AnalyticsEpisodesByCountry.php +++ b/app/Entities/AnalyticsWebsiteByEntryPage.php @@ -1,8 +1,8 @@ <?php /** - * Class AnalyticsEpisodesByCountry - * Entity for AnalyticsEpisodesByCountry + * Class AnalyticsWebsiteByEntryPage + * Entity for AnalyticsWebsiteByEntryPage * @copyright 2020 Podlibre * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ @@ -12,12 +12,11 @@ namespace App\Entities; use CodeIgniter\Entity; -class AnalyticsEpisodesByCountry extends Entity +class AnalyticsWebsiteByEntryPage extends Entity { protected $casts = [ 'podcast_id' => 'integer', - 'episode_id' => 'integer', - 'country_code' => 'string', + 'entry_page' => '?string', 'date' => 'datetime', 'hits' => 'integer', ]; diff --git a/app/Entities/Episode.php b/app/Entities/Episode.php index 7bbda5e53f62fb963ae588897f4128a2c8943a52..26f0ee5c27d2186301e064715a65a8cf5c460911 100644 --- a/app/Entities/Episode.php +++ b/app/Entities/Episode.php @@ -64,6 +64,7 @@ class Episode extends Entity 'enclosure_duration' => 'integer', 'enclosure_mimetype' => 'string', 'enclosure_filesize' => 'integer', + 'enclosure_headersize' => 'integer', 'description' => 'string', 'image_uri' => '?string', 'parental_advisory' => '?string', @@ -143,6 +144,8 @@ class Episode extends Entity $enclosure_metadata['mime_type']; $this->attributes['enclosure_filesize'] = $enclosure_metadata['filesize']; + $this->attributes['enclosure_headersize'] = + $enclosure_metadata['avdataoffset']; return $this; } @@ -167,6 +170,19 @@ class Episode extends Entity 'analytics_hit', $this->attributes['podcast_id'], $this->attributes['id'], + // bytes_threshold: number of bytes that must be downloaded for an episode to be counted in download analytics + // - if file is shorter than 60sec, then it's enclosure_filesize + // - if file is longer than 60 seconds then it's enclosure_headersize + 60 seconds + $this->attributes['enclosure_duration'] <= 60 + ? $this->attributes['enclosure_filesize'] + : $this->attributes['enclosure_headersize'] + + floor( + (($this->attributes['enclosure_filesize'] - + $this->attributes['enclosure_headersize']) / + $this->attributes['enclosure_duration']) * + 60 + ), + $this->attributes['enclosure_filesize'], $this->attributes['enclosure_uri'] ) ); diff --git a/app/Entities/Podcast.php b/app/Entities/Podcast.php index c0dd2b7defbf5c961a792f1c74edfafec060a582..f60e44dc2b8f20552cff94a23b4fa96926b95e23 100644 --- a/app/Entities/Podcast.php +++ b/app/Entities/Podcast.php @@ -82,6 +82,7 @@ class Podcast extends Entity 'created_by' => 'integer', 'updated_by' => 'integer', 'imported_feed_url' => '?string', + 'new_feed_url' => '?string', ]; /** diff --git a/app/Helpers/analytics_helper.php b/app/Helpers/analytics_helper.php index d0ca06cb1c3407d18b95ff6d53fe201eb2a3c994..51561e954a8f152aa10ea5c189d3c3b9e9bd6c26 100644 --- a/app/Helpers/analytics_helper.php +++ b/app/Helpers/analytics_helper.php @@ -33,25 +33,56 @@ if (!function_exists('getallheaders')) { /** * Set user country in session variable, for analytics purpose */ -function set_user_session_country() +function set_user_session_deny_list_ip() { $session = \Config\Services::session(); $session->start(); - $country = 'N/A'; + if (!$session->has('denyListIp')) { + $session->set( + 'denyListIp', + \Podlibre\Ipcat\IpDb::find($_SERVER['REMOTE_ADDR']) != null + ); + } +} - // Finds country: - if (!$session->has('country')) { +/** + * Set user country in session variable, for analytics purpose + */ +function set_user_session_location() +{ + $session = \Config\Services::session(); + $session->start(); + + $location = [ + 'countryCode' => 'N/A', + 'regionCode' => 'N/A', + 'latitude' => null, + 'longitude' => null, + ]; + + // Finds location: + if (!$session->has('location')) { try { - $reader = new \GeoIp2\Database\Reader( - WRITEPATH . 'uploads/GeoLite2-Country/GeoLite2-Country.mmdb' + $cityReader = new \GeoIp2\Database\Reader( + WRITEPATH . 'uploads/GeoLite2-City/GeoLite2-City.mmdb' ); - $geoip = $reader->country($_SERVER['REMOTE_ADDR']); - $country = $geoip->country->isoCode; + $city = $cityReader->city($_SERVER['REMOTE_ADDR']); + + $location = [ + 'countryCode' => empty($city->country->isoCode) + ? 'N/A' + : $city->country->isoCode, + 'regionCode' => empty($city->subdivisions[0]->isoCode) + ? 'N/A' + : $city->subdivisions[0]->isoCode, + 'latitude' => round($city->location->latitude, 3), + 'longitude' => round($city->location->longitude, 3), + ]; } catch (\Exception $e) { // If things go wrong the show must go on and the user must be able to download the file } - $session->set('country', $country); + $session->set('location', $location); } } @@ -67,58 +98,36 @@ function set_user_session_player() $session = \Config\Services::session(); $session->start(); - $playerName = '- Unknown Player -'; - - $useragent = $_SERVER['HTTP_USER_AGENT']; + $playerFound = null; + $userAgent = $_SERVER['HTTP_USER_AGENT']; try { - $jsonUserAgents = json_decode( - file_get_contents( - WRITEPATH . 'uploads/user-agents/src/user-agents.json' - ), - true - ); - - //Search for current HTTP_USER_AGENT in json file: - foreach ($jsonUserAgents as $player) { - foreach ($player['user_agents'] as $useragentsRegexp) { - //Does the HTTP_USER_AGENT match this regexp: - if (preg_match("#{$useragentsRegexp}#", $useragent)) { - if (isset($player['bot'])) { - //It’s a bot! - $playerName = '- Bot -'; - } else { - //It isn’t a bot, we store device/os/app: - $playerName = - (isset($player['device']) - ? $player['device'] . '/' - : '') . - (isset($player['os']) - ? $player['os'] . '/' - : '') . - (isset($player['app']) ? $player['app'] : '?'); - } - //We found it! - break 2; - } - } - } + $playerFound = \Podlibre\UserAgentsPhp\UserAgents::find($userAgent); } catch (\Exception $e) { // If things go wrong the show must go on and the user must be able to download the file } - if ($playerName == '- Unknown Player -') { + if ($playerFound) { + $session->set('player', $playerFound); + } else { + $session->set('player', [ + 'app' => '- unknown -', + 'device' => '', + 'os' => '', + 'bot' => 0, + ]); // Add to unknown list try { $db = \Config\Database::connect(); - $procedureNameAUU = $db->prefixTable( + $procedureNameAnalyticsUnknownUseragents = $db->prefixTable( 'analytics_unknown_useragents' ); - $db->query("CALL $procedureNameAUU(?)", [$useragent]); + $db->query("CALL $procedureNameAnalyticsUnknownUseragents(?)", [ + $userAgent, + ]); } catch (\Exception $e) { // If things go wrong the show must go on and the user must be able to download the file } } - $session->set('player', $playerName); } } @@ -165,49 +174,149 @@ function set_user_session_referer() } } +/** + * Set user entry page in session variable, for analytics purpose + */ +function set_user_session_entry_page() +{ + $session = \Config\Services::session(); + $session->start(); + + $entryPage = $_SERVER['REQUEST_URI']; + if (!$session->has('entryPage')) { + $session->set('entryPage', $entryPage); + } +} + function webpage_hit($podcast_id) { $session = \Config\Services::session(); $session->start(); - $db = \Config\Database::connect(); - - $procedureName = $db->prefixTable('analytics_website'); - $db->query("call $procedureName(?,?,?,?)", [ - $podcast_id, - $session->get('country'), - $session->get('browser'), - $session->get('referer'), - ]); + + if (!$session->get('denyListIp')) { + $db = \Config\Database::connect(); + + $referer = $session->get('referer'); + $domain = empty(parse_url($referer, PHP_URL_HOST)) + ? null + : parse_url($referer, PHP_URL_HOST); + parse_str(parse_url($referer, PHP_URL_QUERY), $queries); + $keywords = empty($queries['q']) ? null : $queries['q']; + + $procedureName = $db->prefixTable('analytics_website'); + $db->query("call $procedureName(?,?,?,?,?,?)", [ + $podcast_id, + $session->get('browser'), + $session->get('entryPage'), + $referer, + $domain, + $keywords, + ]); + } } -function podcast_hit($p_podcast_id, $p_episode_id) +/** + * Counting podcast episode downloads for analytics purposes + * ✅ No IP address is ever stored on the server. + * ✅ Only aggregate data is stored in the database. + * We follow IAB Podcast Measurement Technical Guidelines Version 2.0: + * https://iabtechlab.com/standards/podcast-measurement-guidelines/ + * https://iabtechlab.com/wp-content/uploads/2017/12/Podcast_Measurement_v2-Dec-20-2017.pdf + * ✅ Rolling 24-hour window + * ✅ Castopod does not do pre-load + * ✅ IP deny list https://github.com/client9/ipcat + * ✅ User-agent Filtering https://github.com/opawg/user-agents + * ✅ Ignores 2 bytes range "Range: 0-1" (performed by official Apple iOS Podcast app) + * ✅ In case of partial content, adds up all requests to check >1mn was downloaded + * ✅ Identifying Uniques is done with a combination of IP Address and User Agent + * @param int $podcastId The podcast ID + * @param int $episodeId The Episode ID + * @param int $bytesThreshold The minimum total number of bytes that must be downloaded so that an episode is counted (>1mn) + * @param int $fileSize The podcast complete file size + * + * @return void + */ +function podcast_hit($podcastId, $episodeId, $bytesThreshold, $fileSize) { $session = \Config\Services::session(); $session->start(); - $first_time_for_this_episode = true; - if ($session->has('episodes')) { - if (in_array($p_episode_id, $session->get('episodes'))) { - $first_time_for_this_episode = false; + // We try to count (but if things went wrong the show should go on and the user should be able to download the file): + try { + // If the user IP is denied it's probably a bot: + if ($session->get('denyListIp')) { + $session->get('player')['bot'] = true; + } + $httpRange = $_SERVER['HTTP_RANGE']; + // We create a sha1 hash for this IP_Address+User_Agent+Episode_ID: + $hashID = + '_IpUaEp_' . + sha1( + $_SERVER['REMOTE_ADDR'] . + '_' . + $_SERVER['HTTP_USER_AGENT'] . + '_' . + $episodeId + ); + // Was this episode downloaded in the past 24h: + $downloadedBytes = cache($hashID); + // Rolling window is 24 hours (86400 seconds): + $ttl = 86400; + if ($downloadedBytes) { + // In case it was already downloaded, TTL should be adjusted (rolling window is 24h since 1st download): + $ttl = cache()->getMetadata($hashID)['expire'] - time(); } else { - $session->push('episodes', [$p_episode_id]); + // If it was never downloaded that means that zero byte were downloaded: + $downloadedBytes = 0; } - } else { - $session->set('episodes', [$p_episode_id]); - } + // If the number of downloaded bytes was previously below the 1mn threshold we go on: + // (Otherwise it means that this was already counted, therefore we don't do anything) + if ($downloadedBytes < $bytesThreshold) { + // If HTTP_RANGE is null we are downloading the complete file: + if (!isset($httpRange)) { + $downloadedBytes = $fileSize; + } else { + // [0-1] bytes range requests are used (by Apple) to check that file exists and that 206 partial content is working. + // We don't count these requests: + if ($httpRange != 'bytes=0-1') { + // We calculate how many bytes are being downloaded based on HTTP_RANGE values: + $ranges = explode(',', substr($httpRange, 6)); + foreach ($ranges as $range) { + $parts = explode('-', $range); + $downloadedBytes += empty($parts[1]) + ? $fileSize + : $parts[1] - (empty($parts[0]) ? 0 : $parts[0]); + } + } + } + // We save the number of downloaded bytes for this user and this episode: + cache()->save($hashID, $downloadedBytes, $ttl); - if ($first_time_for_this_episode) { - $db = \Config\Database::connect(); - $procedureName = $db->prefixTable('analytics_podcasts'); - try { - $db->query("CALL $procedureName(?,?,?,?);", [ - $p_podcast_id, - $p_episode_id, - $session->get('country'), - $session->get('player'), - ]); - } catch (\Exception $e) { - // If things go wrong the show must go on and the user must be able to download the file + // If more that 1mn was downloaded, we send that to the database: + if ($downloadedBytes >= $bytesThreshold) { + $db = \Config\Database::connect(); + $procedureName = $db->prefixTable('analytics_podcasts'); + + $app = $session->get('player')['app']; + $device = $session->get('player')['device']; + $os = $session->get('player')['os']; + $bot = $session->get('player')['bot']; + + $db->query("CALL $procedureName(?,?,?,?,?,?,?,?,?,?);", [ + $podcastId, + $episodeId, + $session->get('location')['countryCode'], + $session->get('location')['regionCode'], + $session->get('location')['latitude'], + $session->get('location')['longitude'], + $app == null ? '' : $app, + $device == null ? '' : $device, + $os == null ? '' : $os, + $bot == null ? 0 : $bot, + ]); + } } + } catch (\Exception $e) { + // If things go wrong the show must go on and the user must be able to download the file } } diff --git a/app/Helpers/id3_helper.php b/app/Helpers/id3_helper.php index a7c1685a54236f07bbc0d24520c19e83c2abc596..61c21e2e55d3f6c29fc83a078a5bc07444e30a94 100644 --- a/app/Helpers/id3_helper.php +++ b/app/Helpers/id3_helper.php @@ -24,6 +24,7 @@ function get_file_tags($file) return [ 'filesize' => $FileInfo['filesize'], 'mime_type' => $FileInfo['mime_type'], + 'avdataoffset' => $FileInfo['avdataoffset'], 'playtime_seconds' => $FileInfo['playtime_seconds'], ]; } @@ -68,7 +69,11 @@ function write_enclosure_tags($episode) 'comment' => [$episode->description], 'track_number' => [strval($episode->number)], 'copyright_message' => [$episode->podcast->copyright], - 'publisher' => ['Podlibre'], + 'publisher' => [ + empty($episode->podcast->publisher) + ? $episode->podcast->owner_name + : $episode->podcast->publisher, + ], 'encoded_by' => ['Castopod'], // TODO: find a way to add the remaining tags for podcasts as the library doesn't seem to allow it diff --git a/app/Helpers/rss_helper.php b/app/Helpers/rss_helper.php index 532b9bcb57a57e1ae16001fe109addd3fe1e16d8..9f17a2f73c380065c6413257e02835c365ee598c 100644 --- a/app/Helpers/rss_helper.php +++ b/app/Helpers/rss_helper.php @@ -36,6 +36,14 @@ function get_rss_feed($podcast) $atom_link->addAttribute('rel', 'self'); $atom_link->addAttribute('type', 'application/rss+xml'); + if (!empty($podcast->new_feed_url)) { + $channel->addChild( + 'new-feed-url', + $podcast->new_feed_url, + $itunes_namespace + ); + } + // the last build date corresponds to the creation of the feed.xml cache $channel->addChild( 'lastBuildDate', @@ -50,7 +58,7 @@ function get_rss_feed($podcast) $channel->addChild('title', $podcast->title); $channel->addChildWithCDATA('description', $podcast->description_html); $itunes_image = $channel->addChild('image', null, $itunes_namespace); - $itunes_image->addAttribute('href', $podcast->image->url); + $itunes_image->addAttribute('href', $podcast->image->original_url); $channel->addChild('language', $podcast->language); // set main category first, then other categories as apple diff --git a/app/Language/en/Breadcrumb.php b/app/Language/en/Breadcrumb.php index 70e5beb9ceec00fa9b439075d135b3dd8781b220..5827731b8f23af69c99a1b97620dfdf8a2eec995 100644 --- a/app/Language/en/Breadcrumb.php +++ b/app/Language/en/Breadcrumb.php @@ -22,4 +22,5 @@ return [ 'import' => 'feed import', 'settings' => 'settings', 'platforms' => 'platforms', + 'analytics' => 'Analytics', ]; diff --git a/app/Language/en/Podcast.php b/app/Language/en/Podcast.php index 8a86df06c9cef6d47e7dce64b0c127ec9e88cb7d..fc54298c41d571240f96300b36d72e986b6fdc8a 100644 --- a/app/Language/en/Podcast.php +++ b/app/Language/en/Podcast.php @@ -12,7 +12,7 @@ return [ 'create' => 'Create a podcast', 'import' => 'Import a podcast', 'new_episode' => 'New Episode', - 'feed' => 'RSS feed', + 'feed' => 'RSS', 'view' => 'View podcast', 'edit' => 'Edit podcast', 'delete' => 'Delete podcast', diff --git a/app/Language/en/PodcastImport.php b/app/Language/en/PodcastImport.php index 6b86eb1693dfe2dd8ec7cc2f06ea376a2f7ccbf1..3bb3a912c268e7d04b3edb3e781b3269c046cd7a 100644 --- a/app/Language/en/PodcastImport.php +++ b/app/Language/en/PodcastImport.php @@ -7,6 +7,12 @@ */ return [ + 'legal_dislaimer_title' => 'Legal Disclaimer', + 'legal_dislaimer_content' => + 'Make sure you own the rights for this podcast before importing it.<br/>Copying and broadcasting a podcast without the proper rights is piracy and is liable to prosecution.', + 'warning_title' => 'Warning', + 'warning_content' => + 'This procedure may take a long time.<br/>The current version does not show any progress while it runs. You will not see anything updated until it is done.<br/>In case of timeout error, increase max_execution_time value.', 'old_podcast_section_title' => 'The podcast to import', 'old_podcast_section_subtitle' => '', 'imported_feed_url' => 'Feed URL', diff --git a/app/Language/en/PodcastNavigation.php b/app/Language/en/PodcastNavigation.php index 05fe31f198e986913d8a60437d0efba4c71439b6..3d15320696645e456adab4ab314b9631a4e8102d 100644 --- a/app/Language/en/PodcastNavigation.php +++ b/app/Language/en/PodcastNavigation.php @@ -20,4 +20,5 @@ return [ 'contributor-add' => 'Add contributor', 'settings' => 'Settings', 'platforms' => 'Podcast platforms', + 'podcast-analytics' => 'Audiences Overview', ]; diff --git a/app/Models/AnalyticsPodcastsByCountryModel.php b/app/Models/AnalyticsPodcastsByCountryModel.php index 70f5fc3e77ecdc1454447db4565309a31dc74a3a..4f2094532ce8d7d4235a4e202d867899be08f7d9 100644 --- a/app/Models/AnalyticsPodcastsByCountryModel.php +++ b/app/Models/AnalyticsPodcastsByCountryModel.php @@ -2,7 +2,7 @@ /** * Class AnalyticsPodcastsByCountryModel - * Model for analytics_episodes_by_country table in database + * Model for analytics_podcasts_by_country table in database * @copyright 2020 Podlibre * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 * @link https://castopod.org/ @@ -14,8 +14,7 @@ use CodeIgniter\Model; class AnalyticsPodcastsByCountryModel extends Model { - protected $table = 'analytics_episodes_by_country'; - protected $primaryKey = 'id'; + protected $table = 'analytics_podcasts_by_country'; protected $allowedFields = []; diff --git a/app/Models/AnalyticsPodcastsByEpisodeModel.php b/app/Models/AnalyticsPodcastsByEpisodeModel.php new file mode 100644 index 0000000000000000000000000000000000000000..59c82360ddcaa6e0b1d284da93103bbc1d758e48 --- /dev/null +++ b/app/Models/AnalyticsPodcastsByEpisodeModel.php @@ -0,0 +1,113 @@ +<?php + +/** + * Class AnalyticsPodcastsByEpisodeModel + * Model for analytics_podcasts_by_episodes table in database + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Models; + +use CodeIgniter\Model; + +class AnalyticsPodcastsByEpisodeModel extends Model +{ + protected $table = 'analytics_podcasts_by_episode'; + + protected $allowedFields = []; + + protected $returnType = \App\Entities\AnalyticsPodcastsByEpisode::class; + protected $useSoftDeletes = false; + + protected $useTimestamps = false; + + /** + * @param int $podcastId, $episodeId + * + * @return array + */ + public function getDataByDay(int $podcastId, int $episodeId = null): array + { + if (!$episodeId) { + if ( + !($found = cache( + "{$podcastId}_analytics_podcast_by_episode_by_day" + )) + ) { + $lastEpisodes = (new EpisodeModel()) + ->select('id, season_number, number, title') + ->orderBy('id', 'DESC') + ->where(['podcast_id' => $podcastId]) + ->findAll(5); + + $found = $this->select('age AS X'); + + $letter = 97; + foreach ($lastEpisodes as $episode) { + $found = $found + ->selectSum( + '(CASE WHEN `episode_id`=' . + $episode->id . + ' THEN `hits` END)', + chr($letter) . 'Y' + ) + ->select( + '"' . + (empty($episode->season_number) + ? '' + : $episode->season_number) . + (empty($episode->number) + ? '' + : '-' . $episode->number . '/ ') . + $episode->title . + '" AS ' . + chr($letter) . + 'Value' + ); + $letter++; + } + + $found = $found + ->where([ + 'podcast_id' => $podcastId, + 'age <' => 60, + ]) + ->groupBy('X') + ->orderBy('X', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_podcast_by_episode_by_day", + $found, + 14400 + ); + } + return $found; + } else { + if ( + !($found = cache( + "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day" + )) + ) { + $found = $this->select('date as labels') + ->selectSum('hits', 'values') + ->where([ + 'episode_id' => $episodeId, + 'podcast_id' => $podcastId, + ]) + ->groupBy('labels') + ->orderBy('labels', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_{$episodeId}_analytics_podcast_by_episode_by_day", + $found, + 14400 + ); + } + return $found; + } + } +} diff --git a/app/Models/AnalyticsPodcastsByPlayerModel.php b/app/Models/AnalyticsPodcastsByPlayerModel.php index 5e0ff822f592e8f5ccbcd13018878b4b4c9eeb0c..900b44fa3929003df4737288e676450853d7ef74 100644 --- a/app/Models/AnalyticsPodcastsByPlayerModel.php +++ b/app/Models/AnalyticsPodcastsByPlayerModel.php @@ -15,7 +15,6 @@ use CodeIgniter\Model; class AnalyticsPodcastsByPlayerModel extends Model { protected $table = 'analytics_podcasts_by_player'; - protected $primaryKey = 'id'; protected $allowedFields = []; @@ -23,4 +22,120 @@ class AnalyticsPodcastsByPlayerModel extends Model protected $useSoftDeletes = false; protected $useTimestamps = false; + + /** + * Gets all data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataByApp(int $podcastId): array + { + if ( + !($found = cache( + "{$podcastId}_analytics_podcasts_by_player_by_app" + )) + ) { + $found = $this->select('`app` as `labels`') + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`app` !=' => null, + '`bot`' => 0, + '`date` >' => date('Y-m-d', strtotime('-1 week')), + ]) + ->groupBy('`labels`') + ->orderBy('`values``', 'DESC') + ->findAll(10); + + cache()->save( + "{$podcastId}_analytics_podcasts_by_player_by_app", + $found, + 14400 + ); + } + + return $found; + } + + /** + * Gets all data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataByDevice(int $podcastId): array + { + if ( + !($found = cache( + "{$podcastId}_analytics_podcasts_by_player_by_device" + )) + ) { + $foundApp = $this->select( + 'CONCAT_WS("/", `device`, `os`, `app`) as `ids`, `app` as `labels`, CONCAT_WS("/", `device`, `os`) as `parents`' + ) + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`app` !=' => null, + '`bot`' => 0, + '`date` >' => date('Y-m-d', strtotime('-1 week')), + ]) + ->groupBy('`ids`') + ->orderBy('`values``', 'DESC') + ->findAll(); + + $foundOs = $this->select( + 'CONCAT_WS("/", `device`, `os`) as `ids`, `os` as `labels`, `device` as `parents`' + ) + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`os` !=' => null, + '`bot`' => 0, + '`date` >' => date('Y-m-d', strtotime('-1 week')), + ]) + ->groupBy('`ids`') + ->orderBy('`values``', 'DESC') + ->findAll(); + + $foundDevice = $this->select( + '`device` as `ids`, `device` as `labels`, "" as `parents`' + ) + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`device` !=' => null, + '`bot`' => 0, + '`date` >' => date('Y-m-d', strtotime('-1 week')), + ]) + ->groupBy('`ids`') + ->orderBy('`values``', 'DESC') + ->findAll(); + + $foundBot = $this->select( + '"bots" as `ids`, "Bots" as `labels`, "" as `parents`' + ) + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`bot`' => 1, + '`date` >' => date('Y-m-d', strtotime('-1 week')), + ]) + ->groupBy('`ids`') + ->orderBy('`values``', 'DESC') + ->findAll(); + + $found = array_merge($foundApp, $foundOs, $foundDevice, $foundBot); + cache()->save( + "{$podcastId}_analytics_podcasts_by_player_by_device", + $found, + 14400 + ); + } + + return $found; + } } diff --git a/app/Models/AnalyticsPodcastsByRegionModel.php b/app/Models/AnalyticsPodcastsByRegionModel.php new file mode 100644 index 0000000000000000000000000000000000000000..81ab8537f89a322fda5b0b5973c23b2aea736eed --- /dev/null +++ b/app/Models/AnalyticsPodcastsByRegionModel.php @@ -0,0 +1,25 @@ +<?php + +/** + * Class AnalyticsPodcastsByRegionModel + * Model for analytics_podcasts_by_region table in database + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Models; + +use CodeIgniter\Model; + +class AnalyticsPodcastsByRegionModel extends Model +{ + protected $table = 'analytics_podcasts_by_region'; + + protected $allowedFields = []; + + protected $returnType = \App\Entities\AnalyticsPodcastsByRegion::class; + protected $useSoftDeletes = false; + + protected $useTimestamps = false; +} diff --git a/app/Models/AnalyticsPodcastsModel.php b/app/Models/AnalyticsPodcastsModel.php new file mode 100644 index 0000000000000000000000000000000000000000..8ab115a0ea6a38733344ed1748cbbc99eb101cc7 --- /dev/null +++ b/app/Models/AnalyticsPodcastsModel.php @@ -0,0 +1,55 @@ +<?php + +/** + * Class AnalyticsPodcastsModel + * Model for analytics_podcasts table in database + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Models; + +use CodeIgniter\Model; + +class AnalyticsPodcastsModel extends Model +{ + protected $table = 'analytics_podcasts'; + + protected $allowedFields = []; + + protected $returnType = \App\Entities\AnalyticsPodcasts::class; + protected $useSoftDeletes = false; + + protected $useTimestamps = false; + + /** + * Gets all data for a podcast + * + * @param int $podcastId + * + * @return array + */ + public function getDataByDay(int $podcastId): array + { + if (!($found = cache("{$podcastId}_analytics_podcast_by_day"))) { + $found = $this->select('`date` as `labels`') + ->selectSum('`hits`', '`values`') + ->where([ + '`podcast_id`' => $podcastId, + '`date` >' => date('Y-m-d', strtotime('-1 year')), + ]) + ->groupBy('`labels`') + ->orderBy('`labels``', 'ASC') + ->findAll(); + + cache()->save( + "{$podcastId}_analytics_podcast_by_day", + $found, + 14400 + ); + } + + return $found; + } +} diff --git a/app/Models/AnalyticsWebsiteByBrowserModel.php b/app/Models/AnalyticsWebsiteByBrowserModel.php index ceee6b3e09dacc4af51bf7ca26427b00f0f58bf0..85b4fc92fe65d8fde7e19f11d1b4790b5099f84e 100644 --- a/app/Models/AnalyticsWebsiteByBrowserModel.php +++ b/app/Models/AnalyticsWebsiteByBrowserModel.php @@ -15,7 +15,6 @@ use CodeIgniter\Model; class AnalyticsWebsiteByBrowserModel extends Model { protected $table = 'analytics_website_by_browser'; - protected $primaryKey = 'id'; protected $allowedFields = []; diff --git a/app/Models/AnalyticsWebsiteByEntryPageModel.php b/app/Models/AnalyticsWebsiteByEntryPageModel.php new file mode 100644 index 0000000000000000000000000000000000000000..6e7cfa0c9a0e2f31189f79ed3eb744b8ff123e1c --- /dev/null +++ b/app/Models/AnalyticsWebsiteByEntryPageModel.php @@ -0,0 +1,25 @@ +<?php + +/** + * Class AnalyticsWebsiteByEntryPageModel + * Model for analytics_website_by_entry_page table in database + * @copyright 2020 Podlibre + * @license https://www.gnu.org/licenses/agpl-3.0.en.html AGPL3 + * @link https://castopod.org/ + */ + +namespace App\Models; + +use CodeIgniter\Model; + +class AnalyticsWebsiteByEntryPageModel extends Model +{ + protected $table = 'analytics_website_by_entry_page'; + + protected $allowedFields = []; + + protected $returnType = \App\Entities\AnalyticsWebsiteByEntryPage::class; + protected $useSoftDeletes = false; + + protected $useTimestamps = false; +} diff --git a/app/Models/AnalyticsWebsiteByRefererModel.php b/app/Models/AnalyticsWebsiteByRefererModel.php index 108a6847ab3c255085a5e1d3dc2846184140652e..5d60298ce1cee9f526a1c0c3cf3bfd0b09d74ab8 100644 --- a/app/Models/AnalyticsWebsiteByRefererModel.php +++ b/app/Models/AnalyticsWebsiteByRefererModel.php @@ -15,7 +15,6 @@ use CodeIgniter\Model; class AnalyticsWebsiteByRefererModel extends Model { protected $table = 'analytics_website_by_referer'; - protected $primaryKey = 'id'; protected $allowedFields = []; diff --git a/app/Models/EpisodeModel.php b/app/Models/EpisodeModel.php index d72f3f08654f29342656758a134251ed0efeba79..95ca8abb711b51775eb4db8e6f7ef6a161d9d5da 100644 --- a/app/Models/EpisodeModel.php +++ b/app/Models/EpisodeModel.php @@ -24,6 +24,7 @@ class EpisodeModel extends Model 'enclosure_duration', 'enclosure_mimetype', 'enclosure_filesize', + 'enclosure_headersize', 'description', 'image_uri', 'parental_advisory', diff --git a/app/Models/PodcastModel.php b/app/Models/PodcastModel.php index 2df723492184d0ba5ea6b0fc793ce3e7179bad6e..1fd98403bfe7e0eea9b77f8d317e48a0789edd13 100644 --- a/app/Models/PodcastModel.php +++ b/app/Models/PodcastModel.php @@ -35,6 +35,7 @@ class PodcastModel extends Model 'created_by', 'updated_by', 'imported_feed_url', + 'new_feed_url', ]; protected $returnType = \App\Entities\Podcast::class; diff --git a/app/Views/_assets/charts.ts b/app/Views/_assets/charts.ts new file mode 100644 index 0000000000000000000000000000000000000000..de52bd13f433cbd3a41ed9ea2e02d6c4cf5ada0c --- /dev/null +++ b/app/Views/_assets/charts.ts @@ -0,0 +1,4 @@ +import "core-js"; +import DrawCharts from "./modules/Charts"; + +DrawCharts(); diff --git a/app/Views/_assets/images/logo-castopod-circle.svg b/app/Views/_assets/images/logo-castopod-circle.svg new file mode 100644 index 0000000000000000000000000000000000000000..562d13facf3f588e1b9542bf7e5562ae9a63ed71 --- /dev/null +++ b/app/Views/_assets/images/logo-castopod-circle.svg @@ -0,0 +1,26 @@ +<?xml version="1.0" encoding="UTF-8"?> +<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata> +<style type="text/css"> + .st0{fill:#009486;} + .st1{fill:#E7F9E4;} + .st2{fill:none;} + .st3{fill:#E7FFE3;} +</style> +<circle cx="32" cy="32" r="32" fill="#e7f9e4" stroke-width="2.0334"/><g transform="matrix(.24971 0 0 .24971 6.7291 14.595)"> + <path id="dark_greeen_19_" class="st0" d="m181.9 131.7h-32.5s-1.2-2.5-2.5-4.9-4.4-2.3-4.4-2.3h-82.8s-3-0.4-4.5 2.3c-1.6 2.7-2.6 4.9-2.6 4.9h-32c-6.9 0-12.6-5.6-12.6-12.5v-98.9c0-6.9 5.6-12.6 12.5-12.6h161.3c6.9 0 12.6 5.6 12.6 12.5v98.9c0.1 6.9-5.6 12.6-12.5 12.6z"/> + <path class="st1" d="m143.7 34.5h-85.1c-14.6 0-26.5 12-26.5 26.6s11.9 26.5 26.5 26.5h85.1c14.6 0 26.5-11.9 26.5-26.5 0.1-14.8-11.8-26.7-26.5-26.6zm-75.4 34.2s-3.9-2.9-9.4-2.9c-4.1 0-8.9 2.5-8.9 2.5-1.3-1.9-2.1-4.1-2.1-6.6 0-6.3 5.1-11.4 11.4-11.4s11.4 5.1 11.4 11.4c0 2.7-0.9 5.1-2.4 7zm32.9 6.6c-12.5 0-12-9.6-12-9.6-0.2-1.8 2.1-2.4 2.9-1.3 0.4 0.6 0.4 0.6 0.7 1.7 1.7 5.9 8.4 5.6 8.4 5.6s6.7 0.4 8.4-5.6c0.3-1 0.3-1.1 0.7-1.7 0.8-1 3.1-0.5 2.9 1.3 0 0 0.5 9.6-12 9.6zm51.1-6.9s-4.8-2.5-8.9-2.5c-5.5 0-9.4 2.9-9.4 2.9-1.5-1.9-2.4-4.3-2.4-7 0-6.3 5.1-11.4 11.4-11.4s11.4 5.1 11.4 11.4c0.1 2.4-0.7 4.7-2.1 6.6z"/> + <path class="st2" d="m110.3 64.3c-0.4 0.6-0.4 0.6-0.7 1.7-1.7 5.9-8.4 5.6-8.4 5.6s-6.7 0.4-8.4-5.6c-0.3-1-0.3-1.1-0.7-1.7-0.8-1-3.1-0.5-2.9 1.3 0 0-0.5 9.6 12 9.6s12-9.6 12-9.6c0.2-1.7-2.1-2.3-2.9-1.3z"/> + <path class="st2" d="m143.1 50.4c-6.3 0-11.4 5.1-11.4 11.4 0 2.6 0.9 5 2.4 7 0 0 3.9-2.9 9.4-2.9 4.1 0 8.9 2.5 8.9 2.5 1.3-1.9 2.1-4.1 2.1-6.6 0-6.3-5.1-11.4-11.4-11.4z"/> + <path class="st2" d="m59.3 50.4c-6.3 0-11.4 5.1-11.4 11.4 0 2.5 0.8 4.7 2.1 6.6 0 0 4.8-2.5 8.9-2.5 5.5 0 9.4 2.9 9.4 2.9 1.5-1.9 2.4-4.3 2.4-7 0-6.3-5.1-11.4-11.4-11.4z"/> + + + <path class="st3" d="m47.1 23.3c-6.3-1.7-11.7 2.1-14.7 7.3-0.7 1.2-0.2 2.2 0.5 2.6 1 0.3 1.7 0.1 2.8-1.5 2.2-3.9 5.9-6.1 10.1-5.3 0 0 2.9 0.9 3.3-1 0.3-1.2-0.8-1.8-2-2.1z"/> + + + + + <path class="st3" d="m159.9 27.3c-0.1 1.9 2.9 1.9 2.9 1.9 4.2 0.4 6.8 2.3 7.8 6.7 0.6 1.9 1.2 2.2 2.3 2.2 0.8-0.1 1.6-1 1.2-2.4-1.4-5.8-5.1-9.8-11.7-9.9-1.2-0.1-2.4 0.2-2.5 1.5z"/> + + +</g> +</svg> diff --git a/app/Views/_assets/images/logo-castopod.svg b/app/Views/_assets/images/logo-castopod.svg index 191b6cc98da66a3e9544ee263650e3306794d02b..0208232af0c03cea02d2bde25bf9c508340e0fbb 100644 --- a/app/Views/_assets/images/logo-castopod.svg +++ b/app/Views/_assets/images/logo-castopod.svg @@ -1,86 +1,40 @@ -<?xml version="1.0" encoding="UTF-8" standalone="no"?> -<svg - xmlns:dc="http://purl.org/dc/elements/1.1/" - xmlns:cc="http://creativecommons.org/ns#" - xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" - xmlns:svg="http://www.w3.org/2000/svg" - xmlns="http://www.w3.org/2000/svg" - xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" - xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" - inkscape:version="1.0beta1 (ee59332, 2019-11-28)" - sodipodi:docname="castopod.svg" - id="svg839" - version="1.1" - viewBox="0 0 64 63.999998" - height="64" - width="64"> - <metadata - id="metadata845"> - <rdf:RDF> - <cc:Work - rdf:about=""> - <dc:format>image/svg+xml</dc:format> - <dc:type - rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> - <dc:title></dc:title> - </cc:Work> - </rdf:RDF> - </metadata> - <defs - id="defs843" /> - <sodipodi:namedview - inkscape:current-layer="svg839" - inkscape:window-maximized="0" - inkscape:window-y="23" - inkscape:window-x="0" - inkscape:cy="33.560512" - inkscape:cx="32" - inkscape:zoom="8.9714173" - showgrid="false" - id="namedview841" - inkscape:window-height="1035" - inkscape:window-width="1920" - inkscape:pageshadow="2" - inkscape:pageopacity="0" - guidetolerance="10" - gridtolerance="10" - objecttolerance="10" - borderopacity="1" - inkscape:document-rotation="0" - bordercolor="#666666" - pagecolor="#ffffff" /> - <circle - id="greencircle" - fill="#37c837" - cx="32" - cy="32" - r="31.684" /> - <g - id="speak"> - <path - d="M45.21 20.22H18.79c-6.473 0-11.74 5.266-11.74 11.74S12.317 43.7 18.79 43.7h10.756c1.08 0 1.957-.875 1.957-1.956 0-1.08-.877-1.957-1.957-1.957H18.79c-4.315 0-7.826-3.51-7.826-7.827 0-4.316 3.51-7.828 7.827-7.828h26.42c4.315 0 7.826 3.512 7.826 7.828 0 4.316-3.51 7.827-7.827 7.827H43.34v.002c-5.41.096-9.783 4.527-9.783 9.96 0 1.08.875 1.957 1.956 1.957 1.08 0 1.956-.876 1.956-1.957 0-3.336 2.714-6.05 6.05-6.05h1.687c6.473 0 11.74-5.266 11.74-11.74s-5.267-11.74-11.74-11.74" - fill="#fff" - id="phylactery" /> - <g - id="threedots"> - <circle - r="2" - cy="32" - cx="24.256159" - id="leftdot" - style="fill:#ffffff;fill-opacity:1;stroke:none;" /> - <circle - style="fill:#ffffff;fill-opacity:1;stroke:none;" - id="middledot" - cx="32" - cy="32" - r="2" /> - <circle - r="2" - cy="32" - cx="39.743839" - id="rightdot" - style="fill:#ffffff;fill-opacity:1;stroke:none;" /> - </g> - </g> +<?xml version="1.0" encoding="utf-8"?> +<!-- Generator: Adobe Illustrator 24.3.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) --> +<svg version="1.1" id="Calque_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" + viewBox="0 0 202.4 137.8" style="enable-background:new 0 0 202.4 137.8;" xml:space="preserve"> +<style type="text/css"> + .st0{fill:#009486;} + .st1{fill:#E7F9E4;} + .st2{fill:none;} + .st3{fill:#E7FFE3;} +</style> +<g> + <path id="dark_greeen_19_" class="st0" d="M181.9,131.7h-32.5c0,0-1.2-2.5-2.5-4.9c-1.3-2.4-4.4-2.3-4.4-2.3H59.7 + c0,0-3-0.4-4.5,2.3c-1.6,2.7-2.6,4.9-2.6,4.9H20.6c-6.9,0-12.6-5.6-12.6-12.5V20.3c0-6.9,5.6-12.6,12.5-12.6h161.3 + c6.9,0,12.6,5.6,12.6,12.5v98.9C194.5,126,188.8,131.7,181.9,131.7z"/> + <path class="st1" d="M143.7,34.5H58.6c-14.6,0-26.5,12-26.5,26.6c0,14.6,11.9,26.5,26.5,26.5h85.1c14.6,0,26.5-11.9,26.5-26.5 + C170.3,46.3,158.4,34.4,143.7,34.5z M68.3,68.7c0,0-3.9-2.9-9.4-2.9c-4.1,0-8.9,2.5-8.9,2.5c-1.3-1.9-2.1-4.1-2.1-6.6 + c0-6.3,5.1-11.4,11.4-11.4s11.4,5.1,11.4,11.4C70.7,64.4,69.8,66.8,68.3,68.7z M101.2,75.3c-12.5,0-12-9.6-12-9.6 + c-0.2-1.8,2.1-2.4,2.9-1.3c0.4,0.6,0.4,0.6,0.7,1.7c1.7,5.9,8.4,5.6,8.4,5.6s6.7,0.4,8.4-5.6c0.3-1,0.3-1.1,0.7-1.7 + c0.8-1,3.1-0.5,2.9,1.3C113.2,65.7,113.7,75.3,101.2,75.3z M152.3,68.4c0,0-4.8-2.5-8.9-2.5c-5.5,0-9.4,2.9-9.4,2.9 + c-1.5-1.9-2.4-4.3-2.4-7c0-6.3,5.1-11.4,11.4-11.4s11.4,5.1,11.4,11.4C154.5,64.2,153.7,66.5,152.3,68.4z"/> + <path class="st2" d="M110.3,64.3c-0.4,0.6-0.4,0.6-0.7,1.7c-1.7,5.9-8.4,5.6-8.4,5.6s-6.7,0.4-8.4-5.6c-0.3-1-0.3-1.1-0.7-1.7 + c-0.8-1-3.1-0.5-2.9,1.3c0,0-0.5,9.6,12,9.6c12.5,0,12-9.6,12-9.6C113.4,63.9,111.1,63.3,110.3,64.3z"/> + <path class="st2" d="M143.1,50.4c-6.3,0-11.4,5.1-11.4,11.4c0,2.6,0.9,5,2.4,7c0,0,3.9-2.9,9.4-2.9c4.1,0,8.9,2.5,8.9,2.5 + c1.3-1.9,2.1-4.1,2.1-6.6C154.5,55.5,149.4,50.4,143.1,50.4z"/> + <path class="st2" d="M59.3,50.4c-6.3,0-11.4,5.1-11.4,11.4c0,2.5,0.8,4.7,2.1,6.6c0,0,4.8-2.5,8.9-2.5c5.5,0,9.4,2.9,9.4,2.9 + c1.5-1.9,2.4-4.3,2.4-7C70.7,55.5,65.6,50.4,59.3,50.4z"/> + <g> + <g> + <path class="st3" d="M47.1,23.3c-6.3-1.7-11.7,2.1-14.7,7.3c-0.7,1.2-0.2,2.2,0.5,2.6c1,0.3,1.7,0.1,2.8-1.5 + c2.2-3.9,5.9-6.1,10.1-5.3c0,0,2.9,0.9,3.3-1C49.4,24.2,48.3,23.6,47.1,23.3z"/> + </g> + </g> + <g> + <g> + <path class="st3" d="M159.9,27.3c-0.1,1.9,2.9,1.9,2.9,1.9c4.2,0.4,6.8,2.3,7.8,6.7c0.6,1.9,1.2,2.2,2.3,2.2 + c0.8-0.1,1.6-1,1.2-2.4c-1.4-5.8-5.1-9.8-11.7-9.9C161.2,25.7,160,26,159.9,27.3z"/> + </g> + </g> +</g> </svg> diff --git a/app/Views/_assets/images/platforms/_default.svg b/app/Views/_assets/images/platforms/_default.svg index b7c7127fa0fd047f5c9152c752a2ec04500f14d8..42640a76d97dc39466729a99b039ee8e81cb707b 100644 --- a/app/Views/_assets/images/platforms/_default.svg +++ b/app/Views/_assets/images/platforms/_default.svg @@ -1,7 +1,26 @@ -<svg id="default" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 300"> - <rect width="300" height="300" rx="67" fill="#ebfaeb"/> - <path id="phylactery" d="M195,112.36H105a40,40,0,0,0,0,80h36.65a6.67,6.67,0,0,0,0-13.33H105a26.67,26.67,0,0,1,0-53.34h90A26.67,26.67,0,0,1,195,179h-6.37A34,34,0,0,0,155.31,213a6.67,6.67,0,1,0,13.33,0,20.64,20.64,0,0,1,20.61-20.61H195a40,40,0,0,0,0-80" fill="#37c837"/> - <circle id="leftdot" cx="123.62" cy="152.5" r="6.81" fill="#37c837"/> - <circle id="middledot" cx="150.01" cy="152.5" r="6.81" fill="#37c837"/> - <circle id="rightdot" cx="176.39" cy="152.5" r="6.81" fill="#37c837"/> +<?xml version="1.0" encoding="UTF-8"?> +<svg width="64" height="64" version="1.1" viewBox="0 0 64 64" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"><metadata><rdf:RDF><cc:Work rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type rdf:resource="http://purl.org/dc/dcmitype/StillImage"/><dc:title/></cc:Work></rdf:RDF></metadata> +<style type="text/css"> + .st0{fill:#AAAAAA;} + .st1{fill:#CCCCCC;} + .st2{fill:none;} + .st3{fill:#EEEEEE;} +</style> +<rect width="64" height="64" fill="#fff" stroke-width=".94495"/><g transform="matrix(.32439 0 0 .32439 -.82859 9.3899)"> + <path id="dark_greeen_19_" class="st0" d="m181.9 131.7h-32.5s-1.2-2.5-2.5-4.9-4.4-2.3-4.4-2.3h-82.8s-3-0.4-4.5 2.3c-1.6 2.7-2.6 4.9-2.6 4.9h-32c-6.9 0-12.6-5.6-12.6-12.5v-98.9c0-6.9 5.6-12.6 12.5-12.6h161.3c6.9 0 12.6 5.6 12.6 12.5v98.9c0.1 6.9-5.6 12.6-12.5 12.6z"/> + <path class="st1" d="m143.7 34.5h-85.1c-14.6 0-26.5 12-26.5 26.6s11.9 26.5 26.5 26.5h85.1c14.6 0 26.5-11.9 26.5-26.5 0.1-14.8-11.8-26.7-26.5-26.6zm-75.4 34.2s-3.9-2.9-9.4-2.9c-4.1 0-8.9 2.5-8.9 2.5-1.3-1.9-2.1-4.1-2.1-6.6 0-6.3 5.1-11.4 11.4-11.4s11.4 5.1 11.4 11.4c0 2.7-0.9 5.1-2.4 7zm32.9 6.6c-12.5 0-12-9.6-12-9.6-0.2-1.8 2.1-2.4 2.9-1.3 0.4 0.6 0.4 0.6 0.7 1.7 1.7 5.9 8.4 5.6 8.4 5.6s6.7 0.4 8.4-5.6c0.3-1 0.3-1.1 0.7-1.7 0.8-1 3.1-0.5 2.9 1.3 0 0 0.5 9.6-12 9.6zm51.1-6.9s-4.8-2.5-8.9-2.5c-5.5 0-9.4 2.9-9.4 2.9-1.5-1.9-2.4-4.3-2.4-7 0-6.3 5.1-11.4 11.4-11.4s11.4 5.1 11.4 11.4c0.1 2.4-0.7 4.7-2.1 6.6z"/> + <path class="st2" d="m110.3 64.3c-0.4 0.6-0.4 0.6-0.7 1.7-1.7 5.9-8.4 5.6-8.4 5.6s-6.7 0.4-8.4-5.6c-0.3-1-0.3-1.1-0.7-1.7-0.8-1-3.1-0.5-2.9 1.3 0 0-0.5 9.6 12 9.6s12-9.6 12-9.6c0.2-1.7-2.1-2.3-2.9-1.3z"/> + <path class="st2" d="m143.1 50.4c-6.3 0-11.4 5.1-11.4 11.4 0 2.6 0.9 5 2.4 7 0 0 3.9-2.9 9.4-2.9 4.1 0 8.9 2.5 8.9 2.5 1.3-1.9 2.1-4.1 2.1-6.6 0-6.3-5.1-11.4-11.4-11.4z"/> + <path class="st2" d="m59.3 50.4c-6.3 0-11.4 5.1-11.4 11.4 0 2.5 0.8 4.7 2.1 6.6 0 0 4.8-2.5 8.9-2.5 5.5 0 9.4 2.9 9.4 2.9 1.5-1.9 2.4-4.3 2.4-7 0-6.3-5.1-11.4-11.4-11.4z"/> + + + <path class="st3" d="m47.1 23.3c-6.3-1.7-11.7 2.1-14.7 7.3-0.7 1.2-0.2 2.2 0.5 2.6 1 0.3 1.7 0.1 2.8-1.5 2.2-3.9 5.9-6.1 10.1-5.3 0 0 2.9 0.9 3.3-1 0.3-1.2-0.8-1.8-2-2.1z"/> + + + + + <path class="st3" d="m159.9 27.3c-0.1 1.9 2.9 1.9 2.9 1.9 4.2 0.4 6.8 2.3 7.8 6.7 0.6 1.9 1.2 2.2 2.3 2.2 0.8-0.1 1.6-1 1.2-2.4-1.4-5.8-5.1-9.8-11.7-9.9-1.2-0.1-2.4 0.2-2.5 1.5z"/> + + +</g> </svg> diff --git a/app/Views/_assets/modules/Charts.ts b/app/Views/_assets/modules/Charts.ts new file mode 100644 index 0000000000000000000000000000000000000000..61acd36168b1336ddbc24620be86a98a8e7fb03a --- /dev/null +++ b/app/Views/_assets/modules/Charts.ts @@ -0,0 +1,134 @@ +// Import modules +import * as am4charts from "@amcharts/amcharts4/charts"; +import * as am4core from "@amcharts/amcharts4/core"; +import am4themes_material from "@amcharts/amcharts4/themes/material"; + +const drawPieChart = (chartDivId: string, dataUrl: string | null): void => { + // Create chart instance + const chart = am4core.create(chartDivId, am4charts.PieChart); + am4core.percent(100); + + // Set theme + am4core.useTheme(am4themes_material); + + chart.innerRadius = am4core.percent(10); + + // Add data + chart.dataSource.url = dataUrl || ""; + chart.dataSource.parser.options.emptyAs = 0; + + // Add and configure Series + const pieSeries = chart.series.push(new am4charts.PieSeries()); + pieSeries.dataFields.value = "values"; + pieSeries.dataFields.category = "labels"; + + pieSeries.slices.template.stroke = am4core.color("#ffffff"); + pieSeries.slices.template.strokeWidth = 1; + pieSeries.slices.template.strokeOpacity = 1; + pieSeries.labels.template.disabled = true; + pieSeries.ticks.template.disabled = true; + + chart.legend = new am4charts.Legend(); + chart.legend.position = "right"; + chart.legend.scrollable = true; +}; + +const drawXYChart = (chartDivId: string, dataUrl: string | null): void => { + // Create chart instance + const chart = am4core.create(chartDivId, am4charts.XYChart); + am4core.percent(100); + + // Set theme + am4core.useTheme(am4themes_material); + + // Create axes + const dateAxis = chart.xAxes.push(new am4charts.DateAxis()); + dateAxis.renderer.minGridDistance = 60; + + chart.yAxes.push(new am4charts.ValueAxis()); + + // Add data + chart.dataSource.url = dataUrl || ""; + chart.dataSource.parser.options.emptyAs = 0; + + // Create series + const series = chart.series.push(new am4charts.LineSeries()); + series.dataFields.valueY = "values"; + series.dataFields.dateX = "labels"; + series.tooltipText = "{valueY} downloads"; + + series.tooltip.pointerOrientation = "vertical"; + + chart.cursor = new am4charts.XYCursor(); + chart.cursor.snapToSeries = series; + chart.cursor.xAxis = dateAxis; + + chart.scrollbarX = new am4core.Scrollbar(); +}; + +const drawXYSeriesChart = ( + chartDivId: string, + dataUrl: string | null +): void => { + // Create chart instance + const chart = am4core.create(chartDivId, am4charts.XYChart); + am4core.percent(100); + + // Set theme + am4core.useTheme(am4themes_material); + + // Create axes + chart.xAxes.push(new am4charts.ValueAxis()); + chart.yAxes.push(new am4charts.ValueAxis()); + + // Add data + chart.dataSource.url = dataUrl || ""; + chart.dataSource.parser.options.emptyAs = 0; + + // Create series + const series1 = chart.series.push(new am4charts.LineSeries()); + series1.dataFields.valueX = "X"; + series1.dataFields.valueY = "aY"; + + const series2 = chart.series.push(new am4charts.LineSeries()); + series2.dataFields.valueX = "X"; + series2.dataFields.valueY = "bY"; + + const series3 = chart.series.push(new am4charts.LineSeries()); + series3.dataFields.valueX = "X"; + series3.dataFields.valueY = "cY"; + + const series4 = chart.series.push(new am4charts.LineSeries()); + series4.dataFields.valueX = "X"; + series4.dataFields.valueY = "dY"; + + const series5 = chart.series.push(new am4charts.LineSeries()); + series5.dataFields.valueX = "X"; + series5.dataFields.valueY = "eY"; +}; + +const DrawCharts = (): void => { + const chartDivs: NodeListOf<HTMLDivElement> = document.querySelectorAll( + "div[data-chart-type]" + ); + + for (let i = 0; i < chartDivs.length; i++) { + const chartDiv: HTMLDivElement = chartDivs[i]; + const chartType = chartDiv.dataset.chartType; + switch (chartType) { + case "pie-chart": + drawPieChart(chartDiv.id, chartDiv.getAttribute("data-chart-url")); + break; + case "xy-chart": + drawXYChart(chartDiv.id, chartDiv.getAttribute("data-chart-url")); + break; + case "xy-series-chart": + drawXYSeriesChart(chartDiv.id, chartDiv.getAttribute("data-chart-url")); + break; + default: + console.error("Unknown chart type:" + chartType); + } + } +}; + +export default DrawCharts; diff --git a/app/Views/_layout.php b/app/Views/_layout.php index c2ce491826353a0c4dcc9c9f0ec0acf743e3cca1..dd3ea6e984c8240ab8dd014b91c487eac451cb8e 100644 --- a/app/Views/_layout.php +++ b/app/Views/_layout.php @@ -24,7 +24,7 @@ <?= render_page_links() ?> <small><?= lang('Common.powered_by', [ 'castopod' => - '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>', + '<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>', ]) ?></small> </footer> </body> diff --git a/app/Views/admin/_layout.php b/app/Views/admin/_layout.php index 8cd3c3f166499691d2e82718a4f07a1444b5a029..d18c245949f167592308d8061db669c1911f76b7 100644 --- a/app/Views/admin/_layout.php +++ b/app/Views/admin/_layout.php @@ -9,7 +9,7 @@ <link rel="shortcut icon" type="image/png" href="/favicon.ico" /> <link rel="stylesheet" href="/assets/admin.css"/> <link rel="stylesheet" href="/assets/index.css"/> - <script src="/assets/admin.js" defer></script> + <script src="/assets/admin.js" type="module" defer></script> </head> <body class="relative bg-gray-100 holy-grail-grid"> @@ -43,12 +43,12 @@ <footer class="px-2 py-2 mx-auto text-xs text-right holy-grail-footer"> <small><?= lang('Common.powered_by', [ 'castopod' => - '<a class="underline hover:no-underline" href="https://castopod.org" target="_blank" rel="noreferrer noopener">Castopod</a>', + '<a class="underline hover:no-underline" href="https://castopod.org/" target="_blank" rel="noreferrer noopener">Castopod</a>', ]) ?></small> </footer> <button type="button" id="sidebar-toggler" - class="fixed bottom-0 left-0 z-50 p-3 mb-3 ml-3 text-xl transition duration-300 ease-in-out bg-white border-2 rounded-full shadow-lg focus:outline-none md:hidden hover:bg-gray-100 focus:shadow-outline" + class="fixed bottom-0 left-0 z-50 p-3 mb-3 ml-3 text-xl transition duration-300 ease-in-out bg-white border-2 rounded-full shadow-lg focus:outline-none md:hidden hover:bg-gray-100 focus:shadow-outline" style="transform: translateX(0px);"><?= icon('menu') ?></button> </body> diff --git a/app/Views/admin/podcast/_sidebar.php b/app/Views/admin/podcast/_sidebar.php index 23bc1f3ffcc52653f87c27fe7b4a410e5d4941eb..570008cc875bf1167db5c071985d3a7d0a0063a4 100644 --- a/app/Views/admin/podcast/_sidebar.php +++ b/app/Views/admin/podcast/_sidebar.php @@ -10,7 +10,7 @@ $podcastNavigation = [ ], 'analytics' => [ 'icon' => 'line-chart', - 'items' => [], + 'items' => ['podcast-analytics'], ], 'contributors' => [ 'icon' => 'group', diff --git a/app/Views/admin/podcast/analytics.php b/app/Views/admin/podcast/analytics.php new file mode 100644 index 0000000000000000000000000000000000000000..c57559b648bbdeaedc52ed29d8622d87841048b4 --- /dev/null +++ b/app/Views/admin/podcast/analytics.php @@ -0,0 +1,32 @@ +<?= $this->extend('admin/_layout') ?> + +<?= $this->section('title') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('pageTitle') ?> +<?= $podcast->title ?> +<?= $this->endSection() ?> + +<?= $this->section('content') ?> +<div class="h-64" id="by-app-pie" data-chart-type="pie-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'PodcastsByPlayer', + 'ByApp' +) ?>"></div> +<div class="h-64" id="by-day-graph" data-chart-type="xy-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'Podcasts', + 'ByDay' +) ?>"></div> +<div class="h-64" id="by-age-graph" data-chart-type="xy-series-chart" data-chart-url="<?= route_to( + 'analytics-data', + $podcast->id, + 'PodcastsByEpisode', + 'ByDay' +) ?>"></div> + +<script src="/assets/charts.js" type="module"></script> +<?= $this->endSection() ?> diff --git a/composer.json b/composer.json index 063237497ac57f8ee0cfdfb4dc3cc6e766afd804..0057dce13b92f4c80e70636c73ce05cc8e497f88 100644 --- a/composer.json +++ b/composer.json @@ -13,7 +13,9 @@ "codeigniter4/codeigniter4": "dev-develop", "league/commonmark": "^1.5", "vlucas/phpdotenv": "^5.2", - "league/html-to-markdown": "^4.10" + "league/html-to-markdown": "^4.10", + "podlibre/user-agents-php": "*", + "podlibre/ipcat": "*" }, "require-dev": { "mikey179/vfsstream": "1.6.*", @@ -27,8 +29,14 @@ }, "scripts": { "test": "phpunit", + "post-install-cmd": [ + "@php vendor/podlibre/user-agents-php/src/UserAgentsGenerate.php > vendor/podlibre/user-agents-php/src/UserAgents.php", + "@php vendor/podlibre/ipcat/IpDbGenerate.php > vendor/podlibre/ipcat/IpDb.php" + ], "post-update-cmd": [ - "@composer dump-autoload" + "@composer dump-autoload", + "@php vendor/podlibre/user-agents-php/src/UserAgentsGenerate.php > vendor/podlibre/user-agents-php/src/UserAgents.php", + "@php vendor/podlibre/ipcat/IpDbGenerate.php > vendor/podlibre/ipcat/IpDb.php" ] }, "support": { diff --git a/composer.lock b/composer.lock index 9e637f246bbbd922a93071b304ed94360402f005..74c6baaf647d972d8771698b44297bb356f04ae4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "38eeae7f5d0143863430cda9df10d487", + "content-hash": "47b9f628f03f8c494a9339b054359ec8", "packages": [ { "name": "codeigniter4/codeigniter4", @@ -12,12 +12,12 @@ "source": { "type": "git", "url": "https://github.com/codeigniter4/CodeIgniter4.git", - "reference": "9204aef421921f2c07021dda418ebfc200fe4a31" + "reference": "ccf68e1d7fc44bfe5abacc39bf16edae45794a83" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/9204aef421921f2c07021dda418ebfc200fe4a31", - "reference": "9204aef421921f2c07021dda418ebfc200fe4a31", + "url": "https://api.github.com/repos/codeigniter4/CodeIgniter4/zipball/ccf68e1d7fc44bfe5abacc39bf16edae45794a83", + "reference": "ccf68e1d7fc44bfe5abacc39bf16edae45794a83", "shasum": "" }, "require": { @@ -37,6 +37,7 @@ "phpstan/phpstan": "^0.12", "phpunit/phpunit": "^8.5", "predis/predis": "^1.1", + "rector/rector": "^0.8", "squizlabs/php_codesniffer": "^3.3" }, "type": "project", @@ -45,6 +46,11 @@ "CodeIgniter\\": "system/" } }, + "autoload-dev": { + "psr-4": { + "Utils\\": "utils" + } + }, "scripts": { "post-update-cmd": [ "@composer dump-autoload", @@ -69,7 +75,7 @@ "slack": "https://codeigniterchat.slack.com", "issues": "https://github.com/codeigniter4/CodeIgniter4/issues" }, - "time": "2020-09-24T17:15:24+00:00" + "time": "2020-10-04T20:15:33+00:00" }, { "name": "composer/ca-bundle", @@ -143,27 +149,27 @@ }, { "name": "geoip2/geoip2", - "version": "v2.10.0", + "version": "v2.11.0", "source": { "type": "git", "url": "https://github.com/maxmind/GeoIP2-php.git", - "reference": "419557cd21d9fe039721a83490701a58c8ce784a" + "reference": "d01be5894a5c1a3381c58c9b1795cd07f96c30f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maxmind/GeoIP2-php/zipball/419557cd21d9fe039721a83490701a58c8ce784a", - "reference": "419557cd21d9fe039721a83490701a58c8ce784a", + "url": "https://api.github.com/repos/maxmind/GeoIP2-php/zipball/d01be5894a5c1a3381c58c9b1795cd07f96c30f7", + "reference": "d01be5894a5c1a3381c58c9b1795cd07f96c30f7", "shasum": "" }, "require": { "ext-json": "*", - "maxmind-db/reader": "~1.5", - "maxmind/web-service-common": "~0.6", - "php": ">=5.6" + "maxmind-db/reader": "~1.8", + "maxmind/web-service-common": "~0.8", + "php": ">=7.2" }, "require-dev": { "friendsofphp/php-cs-fixer": "2.*", - "phpunit/phpunit": "5.*", + "phpunit/phpunit": "^8.0 || ^9.0", "squizlabs/php_codesniffer": "3.*" }, "type": "library", @@ -192,7 +198,7 @@ "geolocation", "maxmind" ], - "time": "2019-12-12T18:48:39+00:00" + "time": "2020-10-01T18:48:34+00:00" }, { "name": "graham-campbell/result-type", @@ -689,23 +695,23 @@ }, { "name": "maxmind-db/reader", - "version": "v1.7.0", + "version": "v1.8.0", "source": { "type": "git", "url": "https://github.com/maxmind/MaxMind-DB-Reader-php.git", - "reference": "942553da239f12051275f9c666538b5dd09e2908" + "reference": "b566d429ac9aec10594b0935be8ff38302f8d5c8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/942553da239f12051275f9c666538b5dd09e2908", - "reference": "942553da239f12051275f9c666538b5dd09e2908", + "url": "https://api.github.com/repos/maxmind/MaxMind-DB-Reader-php/zipball/b566d429ac9aec10594b0935be8ff38302f8d5c8", + "reference": "b566d429ac9aec10594b0935be8ff38302f8d5c8", "shasum": "" }, "require": { "php": ">=7.2" }, "conflict": { - "ext-maxminddb": "<1.7.0,>=2.0.0" + "ext-maxminddb": "<1.8.0,>=2.0.0" }, "require-dev": { "friendsofphp/php-cs-fixer": "2.*", @@ -745,31 +751,31 @@ "geolocation", "maxmind" ], - "time": "2020-08-07T22:10:05+00:00" + "time": "2020-10-01T17:30:21+00:00" }, { "name": "maxmind/web-service-common", - "version": "v0.7.0", + "version": "v0.8.0", "source": { "type": "git", "url": "https://github.com/maxmind/web-service-common-php.git", - "reference": "74c996c218ada5c639c8c2f076756e059f5552fc" + "reference": "ba67d9532cfaf499bd71774b8170d05df4f75fb7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/maxmind/web-service-common-php/zipball/74c996c218ada5c639c8c2f076756e059f5552fc", - "reference": "74c996c218ada5c639c8c2f076756e059f5552fc", + "url": "https://api.github.com/repos/maxmind/web-service-common-php/zipball/ba67d9532cfaf499bd71774b8170d05df4f75fb7", + "reference": "ba67d9532cfaf499bd71774b8170d05df4f75fb7", "shasum": "" }, "require": { "composer/ca-bundle": "^1.0.3", "ext-curl": "*", "ext-json": "*", - "php": ">=5.6" + "php": ">=7.2" }, "require-dev": { "friendsofphp/php-cs-fixer": "2.*", - "phpunit/phpunit": "^4.8.36 || ^5.7 || ^6.5 || ^7.0", + "phpunit/phpunit": "^8.0 || ^9.0", "squizlabs/php_codesniffer": "3.*" }, "type": "library", @@ -791,7 +797,7 @@ ], "description": "Internal MaxMind Web Service API", "homepage": "https://github.com/maxmind/web-service-common-php", - "time": "2020-05-06T14:07:26+00:00" + "time": "2020-10-01T15:28:36+00:00" }, { "name": "myth/auth", @@ -918,6 +924,76 @@ ], "time": "2020-07-20T17:29:33+00:00" }, + { + "name": "podlibre/ipcat", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/podlibre/ipcat.git", + "reference": "1adfc821be508ddc8a742f6a5d5e6e42fdf28e86" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/podlibre/ipcat/zipball/1adfc821be508ddc8a742f6a5d5e6e42fdf28e86", + "reference": "1adfc821be508ddc8a742f6a5d5e6e42fdf28e86", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Podlibre\\Ipcat\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "GPL-3.0-only" + ], + "authors": [ + { + "name": "Benjamin Bellamy", + "email": "ben@podlibre.org", + "homepage": "https://podlibre.org/" + } + ], + "description": "Categorization of IP Addresses forked from https://github.com/client9/ipcat", + "homepage": "https://github.com/podlibre/ipcat", + "time": "2020-10-05T17:15:07+00:00" + }, + { + "name": "podlibre/user-agents-php", + "version": "dev-main", + "source": { + "type": "git", + "url": "https://github.com/podlibre/user-agents-php.git", + "reference": "891066bae6b4881a8b7a57eb72a67fca1fcf67c0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/podlibre/user-agents-php/zipball/891066bae6b4881a8b7a57eb72a67fca1fcf67c0", + "reference": "891066bae6b4881a8b7a57eb72a67fca1fcf67c0", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-4": { + "Podlibre\\UserAgentsPhp\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Benjamin Bellamy", + "email": "ben@podlibre.org", + "homepage": "https://podlibre.org/" + } + ], + "description": "PHP implementation for opawg/user-agents.", + "homepage": "https://github.com/podlibre/user-agents-php", + "time": "2020-10-05T16:58:13+00:00" + }, { "name": "psr/cache", "version": "1.0.1", @@ -1801,28 +1877,28 @@ }, { "name": "phpspec/prophecy", - "version": "1.11.1", + "version": "1.12.1", "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160" + "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/b20034be5efcdab4fb60ca3a29cba2949aead160", - "reference": "b20034be5efcdab4fb60ca3a29cba2949aead160", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8ce87516be71aae9b956f81906aaf0338e0d8a2d", + "reference": "8ce87516be71aae9b956f81906aaf0338e0d8a2d", "shasum": "" }, "require": { "doctrine/instantiator": "^1.2", - "php": "^7.2", - "phpdocumentor/reflection-docblock": "^5.0", + "php": "^7.2 || ~8.0, <8.1", + "phpdocumentor/reflection-docblock": "^5.2", "sebastian/comparator": "^3.0 || ^4.0", "sebastian/recursion-context": "^3.0 || ^4.0" }, "require-dev": { "phpspec/phpspec": "^6.0", - "phpunit/phpunit": "^8.0" + "phpunit/phpunit": "^8.0 || ^9.0 <9.3" }, "type": "library", "extra": { @@ -1860,7 +1936,7 @@ "spy", "stub" ], - "time": "2020-07-08T12:44:21+00:00" + "time": "2020-09-29T09:10:42+00:00" }, { "name": "phpunit/php-code-coverage", diff --git a/docs/setup-development.md b/docs/setup-development.md index 66e02a3c561ca3c74cb52b183c3def8afbd67289..5949b1b64a88016453637656228428bd9afad480 100644 --- a/docs/setup-development.md +++ b/docs/setup-development.md @@ -104,6 +104,13 @@ docker ps -a docker-compose run --rm app php spark migrate -all ``` +In case you need to roll back, use this command: + +``` +# rolls back database schema loading (deletes all tables and their content) +docker-compose run --rm app php spark migrate:rollback +``` + 2. Populate the database with the required data: ```bash diff --git a/package-lock.json b/package-lock.json index ae21fc46fb8b46627eab60f2b188a83ab33ef17e..cb81a0b6e4b537a1b4957f0580d15ad93e3e3805 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,39 @@ "lockfileVersion": 1, "requires": true, "dependencies": { + "@amcharts/amcharts4": { + "version": "4.10.5", + "resolved": "https://registry.npmjs.org/@amcharts/amcharts4/-/amcharts4-4.10.5.tgz", + "integrity": "sha512-H4PlVd4jSsD0V0loCg1Jd1gXMCqMaWwQabjd8qejP2lew2ZiXEaJ8eoIuXXoAakehCq/fewtgNO9lq95hdklvA==", + "requires": { + "@babel/runtime": "^7.6.3", + "core-js": "^3.0.0", + "d3-force": "^2.0.1", + "d3-geo": "^2.0.1", + "d3-geo-projection": "^3.0.0", + "pdfmake": "^0.1.36", + "polylabel": "^1.0.2", + "raf": "^3.4.1", + "regression": "^2.0.1", + "rgbcolor": "^1.0.1", + "stackblur-canvas": "^2.0.0", + "tslib": "^2.0.1", + "venn.js": "^0.2.20", + "xlsx": "^0.16.4" + }, + "dependencies": { + "tslib": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", + "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" + } + } + }, + "@amcharts/amcharts4-geodata": { + "version": "4.1.17", + "resolved": "https://registry.npmjs.org/@amcharts/amcharts4-geodata/-/amcharts4-geodata-4.1.17.tgz", + "integrity": "sha512-ylzshiOq/aMRlVrRq8dOznZP7fp4xg/XkmhTjGm2dN6O1WqvoVBBOOnGyzrmb2gBlqN5zj59PQyOqSDyQUNe/Q==" + }, "@babel/code-frame": { "version": "7.10.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.10.1.tgz", @@ -1220,7 +1253,6 @@ "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.10.4.tgz", "integrity": "sha512-UpTN5yUJr9b4EX2CnGNWIvER7Ab83ibv0pcvvHc4UOdrBI5jb8bj+32cCwPX6xu0mt2daFNjYhoi+X7beH0RSw==", - "dev": true, "requires": { "regenerator-runtime": "^0.13.4" }, @@ -1228,8 +1260,7 @@ "regenerator-runtime": { "version": "0.13.5", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.5.tgz", - "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==", - "dev": true + "integrity": "sha512-ZS5w8CpKFinUzOwW3c83oPeVXoNsrLsaCoLtJvAClH135j/R77RuymhiSErhm2lKcwSCIpmvIWSbDkIfAqKQlA==" } } }, @@ -2465,8 +2496,7 @@ "acorn": { "version": "7.4.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.0.tgz", - "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==", - "dev": true + "integrity": "sha512-+G7P8jJmCHr+S+cLfQxygbWhXy+8YTVGzAkpEbcLo2mLoL7tij/VG41QSHACSf5QgYRhMZYHuNc6drJaO0Da+w==" }, "acorn-jsx": { "version": "5.3.1", @@ -2478,7 +2508,6 @@ "version": "1.8.2", "resolved": "https://registry.npmjs.org/acorn-node/-/acorn-node-1.8.2.tgz", "integrity": "sha512-8mt+fslDufLYntIoPAaIMUe/lrbrehIiwmR3t2k9LljIzoigEPF27eLk2hy8zSGzmR/ogr7zbRKINMo1u0yh5A==", - "dev": true, "requires": { "acorn": "^7.0.0", "acorn-walk": "^7.0.0", @@ -2488,8 +2517,16 @@ "acorn-walk": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true + "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==" + }, + "adler-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/adler-32/-/adler-32-1.2.0.tgz", + "integrity": "sha1-aj5r8KY5ALoVZSgIyxXGgT0aXyU=", + "requires": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + } }, "aggregate-error": { "version": "3.1.0", @@ -2513,12 +2550,42 @@ "uri-js": "^4.2.2" } }, + "align-text": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", + "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", + "requires": { + "kind-of": "^3.0.2", + "longest": "^1.0.1", + "repeat-string": "^1.5.2" + }, + "dependencies": { + "kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", + "requires": { + "is-buffer": "^1.1.5" + } + }, + "longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" + } + } + }, "alphanum-sort": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", "dev": true }, + "amdefine": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/amdefine/-/amdefine-1.0.1.tgz", + "integrity": "sha1-SlKCrBZHKek2Gbz9OtFR+BfOkfU=" + }, "ansi-colors": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", @@ -2582,6 +2649,11 @@ "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", "dev": true }, + "array-from": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/array-from/-/array-from-2.1.1.tgz", + "integrity": "sha1-z+nYwmYoudxa7MYqn12PHzUsEZU=" + }, "array-ify": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", @@ -2612,6 +2684,58 @@ "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true }, + "ast-transform": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/ast-transform/-/ast-transform-0.0.0.tgz", + "integrity": "sha1-dJRAWIh9goPhidlUYAlHvJj+AGI=", + "requires": { + "escodegen": "~1.2.0", + "esprima": "~1.0.4", + "through": "~2.3.4" + }, + "dependencies": { + "escodegen": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.2.0.tgz", + "integrity": "sha1-Cd55Z3kcyVi3+Jot220jRRrzJ+E=", + "requires": { + "esprima": "~1.0.4", + "estraverse": "~1.5.0", + "esutils": "~1.0.0", + "source-map": "~0.1.30" + } + }, + "esprima": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", + "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=" + }, + "estraverse": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-1.5.1.tgz", + "integrity": "sha1-hno+jlip+EYYr7bC3bzZFrfLr3E=" + }, + "esutils": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-1.0.0.tgz", + "integrity": "sha1-gVHTWOIMisx/t0XnRywAJf5JZXA=" + }, + "source-map": { + "version": "0.1.43", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.43.tgz", + "integrity": "sha1-wkvBRspRfBRx9drL4lcbK3+eM0Y=", + "optional": true, + "requires": { + "amdefine": ">=0.0.4" + } + } + } + }, + "ast-types": { + "version": "0.7.8", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.7.8.tgz", + "integrity": "sha1-kC0uDWDQcb3NRtwRXhgJ7RHBOKk=" + }, "astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -2667,6 +2791,27 @@ "object.assign": "^4.1.0" } }, + "babel-runtime": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/babel-runtime/-/babel-runtime-6.26.0.tgz", + "integrity": "sha1-llxwWGaOgrVde/4E/yM3vItWR/4=", + "requires": { + "core-js": "^2.4.0", + "regenerator-runtime": "^0.11.0" + }, + "dependencies": { + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + }, + "regenerator-runtime": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz", + "integrity": "sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg==" + } + } + }, "bail": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", @@ -2676,8 +2821,7 @@ "balanced-match": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true + "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" }, "base": { "version": "0.11.2", @@ -2734,6 +2878,11 @@ } } }, + "base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha1-EQHpVE9KdrG8OybUUsqW16NeeXg=" + }, "big.js": { "version": "5.2.2", "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", @@ -2756,7 +2905,6 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -2791,6 +2939,68 @@ } } }, + "brfs": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brfs/-/brfs-2.0.2.tgz", + "integrity": "sha512-IrFjVtwu4eTJZyu8w/V2gxU7iLTtcHih67sgEdzrhjLBMHp2uYefUBfdM4k2UvcuWMgV7PQDZHSLeNWnLFKWVQ==", + "requires": { + "quote-stream": "^1.0.1", + "resolve": "^1.1.5", + "static-module": "^3.0.2", + "through2": "^2.0.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "brotli": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/brotli/-/brotli-1.3.2.tgz", + "integrity": "sha1-UlqcrU/LqWR119OI9q7LE+7VL0Y=", + "requires": { + "base64-js": "^1.1.2" + }, + "dependencies": { + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + } + } + }, + "browser-resolve": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/browser-resolve/-/browser-resolve-1.11.3.tgz", + "integrity": "sha512-exDi1BYWB/6raKHmDTCicQfTkqwN5fioMFV4j8BsfMU4R2DK/QfZfK7kOVkmWCNANf0snkBzqGqAJBao9gZMdQ==", + "requires": { + "resolve": "1.1.7" + }, + "dependencies": { + "resolve": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.1.7.tgz", + "integrity": "sha1-IDEU2CrSxe2ejgQRs5ModeiJ6Xs=" + } + } + }, + "browserify-optional": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-optional/-/browserify-optional-1.0.1.tgz", + "integrity": "sha1-HhNyLP3g2F8SFnbCpyztUzoBiGk=", + "requires": { + "ast-transform": "0.0.0", + "ast-types": "^0.7.0", + "browser-resolve": "^1.8.1" + } + }, "browserslist": { "version": "4.12.0", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.12.0.tgz", @@ -2803,11 +3013,15 @@ "pkg-up": "^2.0.0" } }, + "buffer-equal": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal/-/buffer-equal-0.0.1.tgz", + "integrity": "sha1-kbx0sR6kBbyRa8aqkI+q+ltKrEs=" + }, "buffer-from": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true + "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==" }, "builtin-modules": { "version": "3.1.0", @@ -2915,6 +3129,25 @@ "integrity": "sha512-MOli1W+nfbPLlKEhInaxhRdp7KVLFxLN5ykwzHgLsLI3H3gs5jjFAK4Eoj3OzzcxCtumDaI8onoVDeQyWaNTkw==", "dev": true }, + "center-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", + "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", + "requires": { + "align-text": "^0.1.3", + "lazy-cache": "^1.0.3" + } + }, + "cfb": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cfb/-/cfb-1.2.0.tgz", + "integrity": "sha512-sXMvHsKCICVR3Naq+J556K+ExBo9n50iKl6LGarlnvuA2035uMlGA/qVrc0wQtow5P1vJEw9UyrKLCbtIKz+TQ==", + "requires": { + "adler-32": "~1.2.0", + "crc-32": "~1.2.0", + "printj": "~1.1.2" + } + }, "chalk": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.0.0.tgz", @@ -3239,6 +3472,11 @@ } } }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + }, "clone-regexp": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz", @@ -3280,6 +3518,22 @@ } } }, + "codepage": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/codepage/-/codepage-1.14.0.tgz", + "integrity": "sha1-jL4lSBMjVZ19MHVxsP/5HnodL5k=", + "requires": { + "commander": "~2.14.1", + "exit-on-epipe": "~1.0.1" + }, + "dependencies": { + "commander": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.14.1.tgz", + "integrity": "sha512-+YR16o3rK53SmWHU3rEM3tPAh2rwb1yPcQX5irVn7mb0gXbwuCCrnkbV5+PBfETdfg1vui07nM6PCG1zndcjQw==" + } + } + }, "collapse-white-space": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", @@ -3444,8 +3698,18 @@ "concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + }, + "concat-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", + "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "requires": { + "buffer-from": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^2.2.2", + "typedarray": "^0.0.6" + } }, "concat-with-sourcemaps": { "version": "1.1.0", @@ -3464,6 +3728,11 @@ } } }, + "contour_plot": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/contour_plot/-/contour_plot-0.0.1.tgz", + "integrity": "sha1-R1hw8DK44zhBKqX8UHiA8L9JXHc=" + }, "conventional-changelog-angular": { "version": "5.0.11", "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-5.0.11.tgz", @@ -3510,7 +3779,6 @@ "version": "1.7.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.7.0.tgz", "integrity": "sha512-4FJkXzKXEDB1snCFZlLP4gpC3JILicCpGbzG9f9G7tGqGCzETQ2hWPrcinA9oU4wtf2biUaEH5065UnMeR33oA==", - "dev": true, "requires": { "safe-buffer": "~5.1.1" } @@ -3524,8 +3792,7 @@ "core-js": { "version": "3.6.5", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz", - "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==", - "dev": true + "integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==" }, "core-js-compat": { "version": "3.6.5", @@ -3548,8 +3815,7 @@ "core-util-is": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", - "dev": true + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" }, "cosmiconfig": { "version": "5.2.1", @@ -3563,6 +3829,15 @@ "parse-json": "^4.0.0" } }, + "crc-32": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/crc-32/-/crc-32-1.2.0.tgz", + "integrity": "sha512-1uBwHxF+Y/4yF5G48fwnKq6QsIXheor3ZLPT80yGBV1oEUwpPojlEhQbWKVw1VwcTQyMGHK1/XMmTjmlsmTTGA==", + "requires": { + "exit-on-epipe": "~1.0.1", + "printj": "~1.1.0" + } + }, "crelt": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.4.tgz", @@ -3599,6 +3874,11 @@ } } }, + "crypto-js": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.3.0.tgz", + "integrity": "sha512-DIT51nX0dCfKltpRiXV+/TVZq+Qq2NgF4644+K7Ttnla7zEzqc+kjJyiB96BHNyUTBxyjzRcZYpUdZa+QAqi6Q==" + }, "css-blank-pseudo": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-0.1.4.tgz", @@ -3954,12 +4234,130 @@ } } }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, + "d3-array": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-2.8.0.tgz", + "integrity": "sha512-6V272gsOeg7+9pTW1jSYOR1QE37g95I3my1hBmY+vOUNHRrk9yt4OTz/gK7PMkVAVDrYYq4mq3grTiZ8iJdNIw==" + }, + "d3-color": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-1.4.1.tgz", + "integrity": "sha512-p2sTHSLCJI2QKunbGb7ocOh7DgTAn8IrLx21QRc/BSnodXM4sv6aLQlnfpvehFMLZEfBc6g9pH9SWQccFYfJ9Q==" + }, + "d3-dispatch": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-2.0.0.tgz", + "integrity": "sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==" + }, + "d3-ease": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-1.0.7.tgz", + "integrity": "sha512-lx14ZPYkhNx0s/2HX5sLFUI3mbasHjSSpwO/KaaNACweVwxUruKyWVcb293wMv1RqTPZyZ8kSZ2NogUZNcLOFQ==" + }, + "d3-force": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/d3-force/-/d3-force-2.1.1.tgz", + "integrity": "sha512-nAuHEzBqMvpFVMf9OX75d00OxvOXdxY+xECIXjW6Gv8BRrXu6gAWbv/9XKrvfJ5i5DCokDW7RYE50LRoK092ew==", + "requires": { + "d3-dispatch": "1 - 2", + "d3-quadtree": "1 - 2", + "d3-timer": "1 - 2" + } + }, + "d3-geo": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/d3-geo/-/d3-geo-2.0.1.tgz", + "integrity": "sha512-M6yzGbFRfxzNrVhxDJXzJqSLQ90q1cCyb3EWFZ1LF4eWOBYxFypw7I/NFVBNXKNqxv1bqLathhYvdJ6DC+th3A==", + "requires": { + "d3-array": ">=2.5" + } + }, + "d3-geo-projection": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/d3-geo-projection/-/d3-geo-projection-3.0.0.tgz", + "integrity": "sha512-1JE+filVbkEX2bT25dJdQ05iA4QHvUwev6o0nIQHOSrNlHCAKfVss/U10vEM3pA4j5v7uQoFdQ4KLbx9BlEbWA==", + "requires": { + "commander": "2", + "d3-array": "1 - 2", + "d3-geo": "1.12.0 - 2", + "resolve": "^1.1.10" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + } + } + }, + "d3-interpolate": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-1.4.0.tgz", + "integrity": "sha512-V9znK0zc3jOPV4VD2zZn0sDhZU3WAE2bmlxdIwwQPPzPjvyLkd8B3JUVdS1IDUFDkWZ72c9qnv1GK2ZagTZ8EA==", + "requires": { + "d3-color": "1" + } + }, + "d3-quadtree": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-quadtree/-/d3-quadtree-2.0.0.tgz", + "integrity": "sha512-b0Ed2t1UUalJpc3qXzKi+cPGxeXRr4KU9YSlocN74aTzp6R/Ud43t79yLLqxHRWZfsvWXmbDWPpoENK1K539xw==" + }, + "d3-selection": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/d3-selection/-/d3-selection-1.4.2.tgz", + "integrity": "sha512-SJ0BqYihzOjDnnlfyeHT0e30k0K1+5sR3d5fNueCNeuhZTnGw4M4o8mqJchSwgKMXCNFo+e2VTChiSJ0vYtXkg==" + }, + "d3-timer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-2.0.0.tgz", + "integrity": "sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==" + }, + "d3-transition": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/d3-transition/-/d3-transition-1.3.2.tgz", + "integrity": "sha512-sc0gRU4PFqZ47lPVHloMn9tlPcv8jxgOQg+0zjhfZXMQuvppjG6YuwdMBE0TuqCZjeJkLecku/l9R0JPcRhaDA==", + "requires": { + "d3-color": "1", + "d3-dispatch": "1", + "d3-ease": "1", + "d3-interpolate": "1", + "d3-selection": "^1.1.0", + "d3-timer": "1" + }, + "dependencies": { + "d3-dispatch": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/d3-dispatch/-/d3-dispatch-1.0.6.tgz", + "integrity": "sha512-fVjoElzjhCEy+Hbn8KygnmMS7Or0a9sI2UzGwoB7cCtvI1XpVN9GpoYlnb3xt2YV66oXYb1fLJ8GMvP4hdU1RA==" + }, + "d3-timer": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-1.0.10.tgz", + "integrity": "sha512-B1JDm0XDaQC+uvo4DT79H0XmBskgS3l6Ve+1SBCfxgmtIb1AVrPIoqd+nPSv+loMX8szQ0sVUhGngL7D5QPiXw==" + } + } + }, "dargs": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-7.0.0.tgz", "integrity": "sha512-2iy1EkLdlBzQGvbweYRFxmFath8+K7+AKB0TlhHWkNuH+TmovaMH/Wp7V7R4u7f4SnX3OgLsU9t1NI9ioDnUpg==", "dev": true }, + "dash-ast": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dash-ast/-/dash-ast-1.0.0.tgz", + "integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==" + }, "debug": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", @@ -3972,8 +4370,7 @@ "decamelize": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", - "dev": true + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" }, "decamelize-keys": { "version": "1.1.0", @@ -4005,11 +4402,23 @@ "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", "dev": true }, + "deep-equal": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", + "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", + "requires": { + "is-arguments": "^1.0.4", + "is-date-object": "^1.0.1", + "is-regex": "^1.0.4", + "object-is": "^1.0.1", + "object-keys": "^1.1.1", + "regexp.prototype.flags": "^1.2.0" + } + }, "deep-is": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", - "dev": true + "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" }, "deepmerge": { "version": "4.2.2", @@ -4020,7 +4429,6 @@ "version": "1.1.3", "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "dev": true, "requires": { "object-keys": "^1.0.12" } @@ -4069,8 +4477,7 @@ "defined": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/defined/-/defined-1.0.0.tgz", - "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=", - "dev": true + "integrity": "sha1-yY2bzvdWdBiOEQlpFRGZ45sfppM=" }, "dependency-graph": { "version": "0.9.0", @@ -4101,6 +4508,11 @@ "minimist": "^1.1.1" } }, + "dfa": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/dfa/-/dfa-1.2.0.tgz", + "integrity": "sha512-ED3jP8saaweFTjeGX8HQPjeC1YYyZs98jGNZx6IiBvxW7JG5v492kamAQB3m2wop07CvU/RQmzcKr6bgcC5D/Q==" + }, "dir-glob": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", @@ -4177,6 +4589,22 @@ "is-obj": "^2.0.0" } }, + "dotignore": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/dotignore/-/dotignore-0.1.2.tgz", + "integrity": "sha512-UGGGWfSauusaVJC+8fgV+NVvBXkCTmVv7sk6nojDZZvuOUNGUy0Zk4UpHQD6EDjS0jpBwcACvH4eofvyzBcRDw==", + "requires": { + "minimatch": "^3.0.4" + } + }, + "duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha1-ixLauHjA1p4+eJEFFmKjL8a93ME=", + "requires": { + "readable-stream": "^2.0.2" + } + }, "electron-to-chromium": { "version": "1.3.459", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.459.tgz", @@ -4240,7 +4668,6 @@ "version": "1.17.6", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.6.tgz", "integrity": "sha512-Fr89bON3WFyUi5EvAeI48QTWX0AyekGgLA8H+c+7fbfCkJwRWRMLd8CQedNEyJuoYYhmtEqY92pgte1FAhBlhw==", - "dev": true, "requires": { "es-to-primitive": "^1.2.1", "function-bind": "^1.1.1", @@ -4258,8 +4685,7 @@ "object-inspect": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", - "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==", - "dev": true + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" } } }, @@ -4267,13 +4693,77 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "dev": true, "requires": { "is-callable": "^1.1.4", "is-date-object": "^1.0.1", "is-symbol": "^1.0.2" } }, + "es5-ext": { + "version": "0.10.53", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.53.tgz", + "integrity": "sha512-Xs2Stw6NiNHWypzRTY1MtaG/uJlwCk8kH81920ma8mvN8Xq1gsfhZvpkImLQArw8AHnv8MT2I45J3c0R8slE+Q==", + "requires": { + "es6-iterator": "~2.0.3", + "es6-symbol": "~3.1.3", + "next-tick": "~1.0.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha1-p96IkUGgWpSwhUQDstCg+/qY87c=", + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, + "es6-map": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", + "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-set": "~0.1.5", + "es6-symbol": "~3.1.1", + "event-emitter": "~0.3.5" + } + }, + "es6-set": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", + "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14", + "es6-iterator": "~2.0.1", + "es6-symbol": "3.1.1", + "event-emitter": "~0.3.5" + }, + "dependencies": { + "es6-symbol": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", + "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } + } + } + }, + "es6-symbol": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.3.tgz", + "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", + "requires": { + "d": "^1.0.1", + "ext": "^1.1.2" + } + }, "escalade": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.0.tgz", @@ -4283,8 +4773,62 @@ "escape-string-regexp": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", - "dev": true + "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" + }, + "escodegen": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-1.14.3.tgz", + "integrity": "sha512-qFcX0XJkdg+PB3xjZZG/wKSuT1PnQWx57+TVSjIMmILd2yC/6ByYElPwJnslDsuWuSAp4AwJGumarAAmJch5Kw==", + "requires": { + "esprima": "^4.0.1", + "estraverse": "^4.2.0", + "esutils": "^2.0.2", + "optionator": "^0.8.1", + "source-map": "~0.6.1" + }, + "dependencies": { + "levn": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", + "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", + "requires": { + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2" + } + }, + "optionator": { + "version": "0.8.3", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", + "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", + "requires": { + "deep-is": "~0.1.3", + "fast-levenshtein": "~2.0.6", + "levn": "~0.3.0", + "prelude-ls": "~1.1.2", + "type-check": "~0.3.2", + "word-wrap": "~1.2.3" + } + }, + "prelude-ls": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", + "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=" + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "optional": true + }, + "type-check": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", + "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", + "requires": { + "prelude-ls": "~1.1.2" + } + } + } }, "eslint": { "version": "7.10.0", @@ -4484,8 +5028,7 @@ "esprima": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==" }, "esquery": { "version": "1.3.1", @@ -4524,8 +5067,12 @@ "estraverse": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==" + }, + "estree-is-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/estree-is-function/-/estree-is-function-1.0.0.tgz", + "integrity": "sha512-nSCWn1jkSq2QAtkaVLJZY2ezwcFO161HVc174zL1KPW3RJ+O6C3eJb8Nx7OXzvhoEv+nLgSR1g71oWUHUDTrJA==" }, "estree-walker": { "version": "1.0.1", @@ -4536,8 +5083,16 @@ "esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==" + }, + "event-emitter": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", + "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", + "requires": { + "d": "1", + "es5-ext": "~0.10.14" + } }, "eventemitter3": { "version": "4.0.7", @@ -4588,6 +5143,11 @@ "clone-regexp": "^2.1.0" } }, + "exit-on-epipe": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/exit-on-epipe/-/exit-on-epipe-1.0.1.tgz", + "integrity": "sha512-h2z5mrROTxce56S+pnvAV890uu7ls7f1kEvVGJbw1OlFH3/mlJ5bkXu0KRyW94v37zzHPiUd55iLn3DA7TjWpw==" + }, "expand-brackets": { "version": "2.1.4", "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", @@ -4647,6 +5207,21 @@ "homedir-polyfill": "^1.0.1" } }, + "ext": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ext/-/ext-1.4.0.tgz", + "integrity": "sha512-Key5NIsUxdqKg3vIsdw9dSuXpPCQ297y6wBjL30edxwPgt2E44WcWBZey/ZvUc6sERLTxKdyCu4gZFmUbk1Q7A==", + "requires": { + "type": "^2.0.0" + }, + "dependencies": { + "type": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/type/-/type-2.1.0.tgz", + "integrity": "sha512-G9absDWvhAWCV2gmF1zKud3OyC61nZDwWvBL2DApaVFogI07CprggiQAOOjvp2NRjYWFzPyu7vwtDrQFq8jeSA==" + } + } + }, "extend": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", @@ -4830,8 +5405,7 @@ "fast-levenshtein": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true + "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=" }, "fastest-levenshtein": { "version": "1.0.12", @@ -4965,12 +5539,129 @@ "integrity": "sha512-dVsPA/UwQ8+2uoFe5GHtiBMu48dWLTdsuEd7CKGlZlD78r1TTWBvDuFaFGKCo/ZfEr95Uk56vZoX86OsHkUeIg==", "dev": true }, + "fmin": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/fmin/-/fmin-0.0.2.tgz", + "integrity": "sha1-Wbu0DUP/3ByUzQClaMQflfGXMBc=", + "requires": { + "contour_plot": "^0.0.1", + "json2module": "^0.0.3", + "rollup": "^0.25.8", + "tape": "^4.5.1", + "uglify-js": "^2.6.2" + }, + "dependencies": { + "ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" + }, + "ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=" + }, + "chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", + "requires": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + } + }, + "rollup": { + "version": "0.25.8", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-0.25.8.tgz", + "integrity": "sha1-v2zoO4dRDRY0Ru6qV37WpvxYNeA=", + "requires": { + "chalk": "^1.1.1", + "minimist": "^1.2.0", + "source-map-support": "^0.3.2" + } + }, + "source-map": { + "version": "0.1.32", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.1.32.tgz", + "integrity": "sha1-yLbBZ3l7pHQKjqMyUhYv8IWRsmY=", + "requires": { + "amdefine": ">=0.0.4" + } + }, + "source-map-support": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.3.3.tgz", + "integrity": "sha1-NJAJd9W6PwfHdX7nLnO7GptTdU8=", + "requires": { + "source-map": "0.1.32" + } + }, + "strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", + "requires": { + "ansi-regex": "^2.0.0" + } + }, + "supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=" + } + } + }, + "fontkit": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/fontkit/-/fontkit-1.8.1.tgz", + "integrity": "sha512-BsNCjDoYRxmNWFdAuK1y9bQt+igIxGtTC9u/jSFjR9MKhmI00rP1fwSvERt+5ddE82544l0XH5mzXozQVUy2Tw==", + "requires": { + "babel-runtime": "^6.26.0", + "brfs": "^2.0.0", + "brotli": "^1.2.0", + "browserify-optional": "^1.0.1", + "clone": "^1.0.4", + "deep-equal": "^1.0.0", + "dfa": "^1.2.0", + "restructure": "^0.5.3", + "tiny-inflate": "^1.0.2", + "unicode-properties": "^1.2.2", + "unicode-trie": "^0.3.0" + }, + "dependencies": { + "unicode-trie": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-0.3.1.tgz", + "integrity": "sha1-1nHd3YkQGgi6w3tqUWEBBgIFIIU=", + "requires": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + } + } + }, + "for-each": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz", + "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==", + "requires": { + "is-callable": "^1.1.3" + } + }, "for-in": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", "dev": true }, + "frac": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/frac/-/frac-1.1.2.tgz", + "integrity": "sha512-w/XBfkibaTl3YDqASwfDUqkna4Z2p9cFSr1aHDt0WoMTECnRfBOv2WArlZILlqgWlmdIlALXGpM2AOhEk5W3IA==" + }, "fragment-cache": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", @@ -4994,8 +5685,7 @@ "fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", - "dev": true + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" }, "fsevents": { "version": "2.1.3", @@ -5007,8 +5697,7 @@ "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==", - "dev": true + "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" }, "functional-red-black-tree": { "version": "1.0.1", @@ -5036,6 +5725,11 @@ "integrity": "sha512-r8EC6NO1sngH/zdD9fiRDLdcgnbayXah+mLgManTaIZJqEC1MZstmnox8KpnI2/fxQwrp5OpCOYWLp4rBl4Jcg==", "dev": true }, + "get-assigned-identifiers": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/get-assigned-identifiers/-/get-assigned-identifiers-1.2.0.tgz", + "integrity": "sha512-mBBwmeGTrxEMO4pMaaf/uUEFHnYtwr8FTe8Y/mer4rcV/bye0qGm6pw1bGZFGStxC5O76c5ZAVBGnqHmOaJpdQ==" + }, "get-caller-file": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", @@ -5086,7 +5780,6 @@ "version": "7.1.6", "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", @@ -5189,7 +5882,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "dev": true, "requires": { "function-bind": "^1.1.1" } @@ -5198,7 +5890,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", - "dev": true, "requires": { "ansi-regex": "^2.0.0" }, @@ -5206,8 +5897,7 @@ "ansi-regex": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true + "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=" } } }, @@ -5220,8 +5910,7 @@ "has-symbols": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==", - "dev": true + "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" }, "has-value": { "version": "1.0.0", @@ -5471,7 +6160,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "dev": true, "requires": { "once": "^1.3.0", "wrappy": "1" @@ -5480,8 +6168,7 @@ "inherits": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "ini": { "version": "1.3.5", @@ -5580,6 +6267,11 @@ "is-decimal": "^1.0.0" } }, + "is-arguments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.0.4.tgz", + "integrity": "sha512-xPh0Rmt8NE65sNzvyUmWgI1tz3mKq74lGA0mL8LYZcoIzKOzDh6HmrYm3d18k60nHerC8A9Km8kYu87zfSFnLA==" + }, "is-arrayish": { "version": "0.3.2", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", @@ -5598,14 +6290,12 @@ "is-buffer": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", - "dev": true + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" }, "is-callable": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.0.tgz", - "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==", - "dev": true + "integrity": "sha512-pyVD9AaGLxtg6srb2Ng6ynWJqkHU9bEM087AKck0w8QwDarTfNcpIYoU8x8Hv2Icm8u6kFJM18Dag8lyqGkviw==" }, "is-color-stop": { "version": "1.1.0", @@ -5644,8 +6334,7 @@ "is-date-object": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==", - "dev": true + "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" }, "is-decimal": { "version": "1.0.4", @@ -5717,6 +6406,11 @@ "integrity": "sha1-Mlj7afeMFNW4FdZkM2tM/7ZEFZE=", "dev": true }, + "is-negative-zero": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.0.tgz", + "integrity": "sha1-lVOxIbD6wohp2p7UWeIMdUN4hGE=" + }, "is-number": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", @@ -5771,7 +6465,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.0.tgz", "integrity": "sha512-iI97M8KTWID2la5uYXlkbSDQIg4F6o1sYboZKKTDpnDQMLtUL86zxhgDet3Q2SriaYsyGqZ6Mn2SjbRKeLHdqw==", - "dev": true, "requires": { "has-symbols": "^1.0.1" } @@ -5807,7 +6500,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "dev": true, "requires": { "has-symbols": "^1.0.1" } @@ -5854,8 +6546,7 @@ "isarray": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" }, "isexe": { "version": "2.0.0", @@ -5942,6 +6633,14 @@ "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", "dev": true }, + "json2module": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/json2module/-/json2module-0.0.3.tgz", + "integrity": "sha1-APtfSpt638PwZHwpyxe80Zeb6bI=", + "requires": { + "rw": "^1.3.2" + } + }, "json5": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", @@ -5978,6 +6677,11 @@ "integrity": "sha512-eYboRV94Vco725nKMlpkn3nV2+96p9c3gKXRsYqAJSswSENvBhN7n5L+uDhY58xQa0UukWsDMTGELzmD8Q+wTA==", "dev": true }, + "lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" + }, "leven": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", @@ -6003,6 +6707,16 @@ "type-check": "~0.4.0" } }, + "linebreak": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.0.2.tgz", + "integrity": "sha512-bJwSRsJeAmaZYnkcwl5sCQNfSDAhBuXxb6L27tb+qkBRtUQSSTUa5bcgCPD6hFEkRNlpWHfK7nFMmcANU7ZP1w==", + "requires": { + "base64-js": "0.0.8", + "brfs": "^2.0.2", + "unicode-trie": "^1.0.0" + } + }, "lines-and-columns": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", @@ -6665,6 +7379,14 @@ "integrity": "sha512-VjFo4P5Whtj4vsLzsYBu5ayHhoHJ0UqNm7ibvShmbmoz7tGi0vXaoJbGdB+GmDMLUdg8DpQXEIeVDAe8MaABvQ==", "dev": true }, + "merge-source-map": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/merge-source-map/-/merge-source-map-1.0.4.tgz", + "integrity": "sha1-pd5GU42uhNQRTMXqArR3KmNGcB8=", + "requires": { + "source-map": "^0.5.6" + } + }, "merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6720,7 +7442,6 @@ "version": "3.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "dev": true, "requires": { "brace-expansion": "^1.1.7" } @@ -6728,8 +7449,7 @@ "minimist": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==" }, "minimist-options": { "version": "4.1.0", @@ -6809,6 +7529,11 @@ "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", "dev": true }, + "next-tick": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", + "integrity": "sha1-yobR/ogoFpsBICCOPchCS524NCw=" + }, "node-emoji": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-1.10.0.tgz", @@ -6933,11 +7658,69 @@ "integrity": "sha512-JPKn0GMu+Fa3zt3Bmr66JhokJU5BaNBIh4ZeTlaCBzrBsOeXzwcKKAK1tbLiPKgvwmPXsDvvLHoWh5Bm7ofIYg==", "dev": true }, + "object-inspect": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.8.0.tgz", + "integrity": "sha512-jLdtEOB112fORuypAyl/50VRVIBIdVQOSUUGQHzJ4xBSbit81zRarz7GThkEFZy1RceYrWYcPcBFPQwHyAc1gA==" + }, + "object-is": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.3.tgz", + "integrity": "sha512-teyqLvFWzLkq5B9ki8FVWA902UER2qkxmdA4nLf+wjOLAWgxzCWZNCxpDq9MvE8MmhWNr+I8w3BN49Vx36Y6Xg==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } + } + }, "object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "dev": true + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" }, "object-visit": { "version": "1.0.1", @@ -6952,7 +7735,6 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.0.tgz", "integrity": "sha512-exHJeq6kBKj58mqGyTQ9DFvrZC/eR6OwxzoM9YRoGBqrXYonaFyGiFMuc9VZrXf7DarreEwMpurG3dd+CNyW5w==", - "dev": true, "requires": { "define-properties": "^1.1.2", "function-bind": "^1.1.1", @@ -6995,7 +7777,6 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "dev": true, "requires": { "wrappy": "1" } @@ -7112,6 +7893,11 @@ "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", "dev": true }, + "pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha1-8/dSL073gjSNqBYbrZ7P1Rv4OnU=" + }, "parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -7174,8 +7960,7 @@ "path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", - "dev": true + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" }, "path-key": { "version": "3.1.1", @@ -7186,8 +7971,7 @@ "path-parse": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==", - "dev": true + "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" }, "path-type": { "version": "4.0.0", @@ -7195,6 +7979,44 @@ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true }, + "pdfkit": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/pdfkit/-/pdfkit-0.11.0.tgz", + "integrity": "sha512-1s9gaumXkYxcVF1iRtSmLiISF2r4nHtsTgpwXiK8Swe+xwk/1pm8FJjYqN7L3x13NsWnGyUFntWcO8vfqq+wwA==", + "requires": { + "crypto-js": "^3.1.9-1", + "fontkit": "^1.8.0", + "linebreak": "^1.0.2", + "png-js": "^1.0.0" + } + }, + "pdfmake": { + "version": "0.1.68", + "resolved": "https://registry.npmjs.org/pdfmake/-/pdfmake-0.1.68.tgz", + "integrity": "sha512-oE1VEjkluro3+QqvLbFgFU/rRgyKdbPy/Fh8SS/nsUxnsiUcm85ChpmD6YD0hQW1E0d3hppAo4Yh+xdXucenIA==", + "requires": { + "iconv-lite": "^0.6.2", + "linebreak": "^1.0.2", + "pdfkit": "^0.11.0", + "svg-to-pdfkit": "^0.1.8", + "xmldoc": "^1.1.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", + "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=" + }, "php-parser": { "version": "github:glayzzle/php-parser#5a0e2e1bf12517bd1c544c0f4e68482d0362a7b5", "from": "github:glayzzle/php-parser#5a0e2e1bf12517bd1c544c0f4e68482d0362a7b5", @@ -7290,6 +8112,19 @@ "semver-compare": "^1.0.0" } }, + "png-js": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/png-js/-/png-js-1.0.0.tgz", + "integrity": "sha512-k+YsbhpA9e+EFfKjTCH3VW6aoKlyNYI6NYdTfDL4CIvFnvsuO84ttonmZE7rc+v23SLTH8XX+5w/Ak9v0xGY4g==" + }, + "polylabel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/polylabel/-/polylabel-1.1.0.tgz", + "integrity": "sha512-bxaGcA40sL3d6M4hH72Z4NdLqxpXRsCFk8AITYg6x1rn1Ei3izf00UMLklerBZTO49aPA3CYrIwVulx2Bce2pA==", + "requires": { + "tinyqueue": "^2.0.3" + } + }, "posix-character-classes": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", @@ -8798,11 +9633,15 @@ "integrity": "sha1-t+PqQkNaTJsnWdmeDyAesZWALuE=", "dev": true }, + "printj": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/printj/-/printj-1.1.2.tgz", + "integrity": "sha512-zA2SmoLaxZyArQTOPj5LXecR+RagfPSU5Kw1qP+jkWeNlrq+eJZyY2oS68SU1Z/7/myXM4lo9716laOFAVStCQ==" + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "dev": true + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, "progress": { "version": "2.0.3", @@ -9003,6 +9842,35 @@ "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true }, + "quote-stream": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/quote-stream/-/quote-stream-1.0.2.tgz", + "integrity": "sha1-hJY/jJwmuULhU/7rU6rnRlK34LI=", + "requires": { + "buffer-equal": "0.0.1", + "minimist": "^1.1.3", + "through2": "^2.0.0" + }, + "dependencies": { + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, + "raf": { + "version": "3.4.1", + "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", + "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "requires": { + "performance-now": "^2.1.0" + } + }, "randombytes": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", @@ -9076,7 +9944,6 @@ "version": "2.3.7", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "dev": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -9173,6 +10040,15 @@ "safe-regex": "^1.1.0" } }, + "regexp.prototype.flags": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", + "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.0-next.1" + } + }, "regexpp": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.1.0.tgz", @@ -9216,6 +10092,11 @@ } } }, + "regression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/regression/-/regression-2.0.1.tgz", + "integrity": "sha1-jSnD6CJKEIUMNeM36FqLL6w7DIc=" + }, "remark": { "version": "12.0.1", "resolved": "https://registry.npmjs.org/remark/-/remark-12.0.1.tgz", @@ -9282,8 +10163,7 @@ "repeat-string": { "version": "1.6.1", "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", - "dev": true + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" }, "replace-ext": { "version": "1.0.0", @@ -9307,7 +10187,6 @@ "version": "1.17.0", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.17.0.tgz", "integrity": "sha512-ic+7JYiV8Vi2yzQGFWOkiZD5Z9z7O2Zhm9XMaTxdJExKasieFCr+yXZ/WmXsckHiKl12ar0y6XiXDx3m4RHn1w==", - "dev": true, "requires": { "path-parse": "^1.0.6" } @@ -9353,6 +10232,22 @@ "signal-exit": "^3.0.2" } }, + "restructure": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/restructure/-/restructure-0.5.4.tgz", + "integrity": "sha1-9U591WNZD7NP1r9Vh2EJrsyyjeg=", + "requires": { + "browserify-optional": "^1.0.0" + } + }, + "resumer": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/resumer/-/resumer-0.0.0.tgz", + "integrity": "sha1-8ej0YeQGS6Oegq883CqMiT0HZ1k=", + "requires": { + "through": "~2.3.4" + } + }, "ret": { "version": "0.1.15", "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", @@ -9377,6 +10272,19 @@ "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", "dev": true }, + "rgbcolor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/rgbcolor/-/rgbcolor-1.0.1.tgz", + "integrity": "sha1-1lBezbMEplldom+ktDMHMGd1lF0=" + }, + "right-align": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", + "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", + "requires": { + "align-text": "^0.1.1" + } + }, "rimraf": { "version": "2.6.3", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", @@ -9582,6 +10490,11 @@ "integrity": "sha512-DEqnSRTDw/Tc3FXf49zedI638Z9onwUotBMiUFKmrO2sdFKIbXamXGQ3Axd4qgphxKB4kw/qP1w5kTxnfU1B9Q==", "dev": true }, + "rw": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/rw/-/rw-1.3.3.tgz", + "integrity": "sha1-P4Yt+pGrdmsUiF700BEkv9oHT7Q=" + }, "rxjs": { "version": "6.6.2", "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.2.tgz", @@ -9594,8 +10507,7 @@ "safe-buffer": { "version": "5.1.2", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, "safe-identifier": { "version": "0.4.2", @@ -9615,14 +10527,26 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" }, "sax": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", - "dev": true + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==" + }, + "scope-analyzer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/scope-analyzer/-/scope-analyzer-2.1.1.tgz", + "integrity": "sha512-azEAihtQ9mEyZGhfgTJy3IbOWEzeOrYbg7NcYEshPKnKd+LZmC3TNd5dmDxbLBsTG/JVWmCp+vDJ03vJjeXMHg==", + "requires": { + "array-from": "^2.1.1", + "dash-ast": "^1.0.0", + "es6-map": "^0.1.5", + "es6-set": "^0.1.5", + "es6-symbol": "^3.1.1", + "estree-is-function": "^1.0.0", + "get-assigned-identifiers": "^1.1.0" + } }, "semver": { "version": "5.7.1", @@ -9680,6 +10604,11 @@ } } }, + "shallow-copy": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/shallow-copy/-/shallow-copy-0.0.1.tgz", + "integrity": "sha1-QV9CcC1z2BAzApLMXuhurhoRoXA=" + }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -9852,8 +10781,7 @@ "source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=", - "dev": true + "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" }, "source-map-resolve": { "version": "0.5.3", @@ -9895,8 +10823,7 @@ "sourcemap-codec": { "version": "1.4.8", "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", - "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==", - "dev": true + "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==" }, "spdx-correct": { "version": "3.1.1", @@ -9971,18 +10898,39 @@ "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, + "ssf": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/ssf/-/ssf-0.11.2.tgz", + "integrity": "sha512-+idbmIXoYET47hH+d7dfm2epdOMUDjqcB4648sTZ+t2JwoyBFL/insLfB/racrDmsKB3diwsDA696pZMieAC5g==", + "requires": { + "frac": "~1.1.2" + } + }, "stable": { "version": "0.1.8", "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", "dev": true }, + "stackblur-canvas": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/stackblur-canvas/-/stackblur-canvas-2.4.0.tgz", + "integrity": "sha512-Z+HixfgYV0ss3C342DxPwc+UvN1SYWqoz7Wsi3xEDWEnaBkSCL3Ey21gF4io+WlLm8/RIrSnCrDBIEcH4O+q5Q==" + }, "state-toggle": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==", "dev": true }, + "static-eval": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/static-eval/-/static-eval-2.1.0.tgz", + "integrity": "sha512-agtxZ/kWSsCkI5E4QifRwsaPs0P0JmZV6dkLz6ILYfFYQGn+5plctanRN+IC8dJRiFkyXHrwEE3W9Wmx67uDbw==", + "requires": { + "escodegen": "^1.11.1" + } + }, "static-extend": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", @@ -10004,6 +10952,46 @@ } } }, + "static-module": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/static-module/-/static-module-3.0.4.tgz", + "integrity": "sha512-gb0v0rrgpBkifXCa3yZXxqVmXDVE+ETXj6YlC/jt5VzOnGXR2C15+++eXuMDUYsePnbhf+lwW0pE1UXyOLtGCw==", + "requires": { + "acorn-node": "^1.3.0", + "concat-stream": "~1.6.0", + "convert-source-map": "^1.5.1", + "duplexer2": "~0.1.4", + "escodegen": "^1.11.1", + "has": "^1.0.1", + "magic-string": "0.25.1", + "merge-source-map": "1.0.4", + "object-inspect": "^1.6.0", + "readable-stream": "~2.3.3", + "scope-analyzer": "^2.0.1", + "shallow-copy": "~0.0.1", + "static-eval": "^2.0.5", + "through2": "~2.0.3" + }, + "dependencies": { + "magic-string": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.1.tgz", + "integrity": "sha512-sCuTz6pYom8Rlt4ISPFn6wuFodbKMIHUMv4Qko9P17dpxb7s52KJTmRuZZqHdGmLCK9AOcDare039nRIcfdkEg==", + "requires": { + "sourcemap-codec": "^1.4.1" + } + }, + "through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "requires": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + } + } + }, "string-argv": { "version": "0.3.1", "resolved": "https://registry.npmjs.org/string-argv/-/string-argv-0.3.1.tgz", @@ -10037,11 +11025,64 @@ } } }, + "string.prototype.trim": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.2.tgz", + "integrity": "sha512-b5yrbl3BXIjHau9Prk7U0RRYcUYdN4wGSVaqoBQS50CCE3KBuYU0TYRNPFCP7aVoNMX87HKThdMRVIP3giclKg==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0" + }, + "dependencies": { + "es-abstract": { + "version": "1.18.0-next.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", + "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", + "requires": { + "es-to-primitive": "^1.2.1", + "function-bind": "^1.1.1", + "has": "^1.0.3", + "has-symbols": "^1.0.1", + "is-callable": "^1.2.2", + "is-negative-zero": "^2.0.0", + "is-regex": "^1.1.1", + "object-inspect": "^1.8.0", + "object-keys": "^1.1.1", + "object.assign": "^4.1.1", + "string.prototype.trimend": "^1.0.1", + "string.prototype.trimstart": "^1.0.1" + } + }, + "is-callable": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", + "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" + }, + "is-regex": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", + "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", + "requires": { + "has-symbols": "^1.0.1" + } + }, + "object.assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.1.tgz", + "integrity": "sha512-VT/cxmx5yaoHSOTSyrCygIDFco+RsibY2NM0a4RdEeY/4KgqezwFtK1yr3U67xYhqJSlASm2pKhLVzPj2lr4bA==", + "requires": { + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.0", + "has-symbols": "^1.0.1", + "object-keys": "^1.1.1" + } + } + } + }, "string.prototype.trimend": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.1.tgz", "integrity": "sha512-LRPxFUaTtpqYsTeNKaFOw3R4bxIzWOnbQ837QfBylo8jIxtcbK/A/sMV7Q+OAV/vWo+7s25pOE10KYSjaSO06g==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -10051,7 +11092,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.1.tgz", "integrity": "sha512-XxZn+QpvrBI1FOcg6dIpxUPgWCPuNXvMD72aaRaUQv1eD4e/Qy8i/hFTe0BUmD60p/QA6bh1avmuPTfNjqVWRw==", - "dev": true, "requires": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" @@ -10061,7 +11101,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "dev": true, "requires": { "safe-buffer": "~5.1.0" } @@ -10548,6 +11587,14 @@ "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=", "dev": true }, + "svg-to-pdfkit": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/svg-to-pdfkit/-/svg-to-pdfkit-0.1.8.tgz", + "integrity": "sha512-QItiGZBy5TstGy+q8mjQTMGRlDDOARXLxH+sgVm1n/LYeo0zFcQlcCh8m4zi8QxctrxB9Kue/lStc/RD5iLadQ==", + "requires": { + "pdfkit": ">=0.8.1" + } + }, "svgo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", @@ -10648,6 +11695,43 @@ "resolve": "^1.14.2" } }, + "tape": { + "version": "4.13.3", + "resolved": "https://registry.npmjs.org/tape/-/tape-4.13.3.tgz", + "integrity": "sha512-0/Y20PwRIUkQcTCSi4AASs+OANZZwqPKaipGCEwp10dQMipVvSZwUUCi01Y/OklIGyHKFhIcjock+DKnBfLAFw==", + "requires": { + "deep-equal": "~1.1.1", + "defined": "~1.0.0", + "dotignore": "~0.1.2", + "for-each": "~0.3.3", + "function-bind": "~1.1.1", + "glob": "~7.1.6", + "has": "~1.0.3", + "inherits": "~2.0.4", + "is-regex": "~1.0.5", + "minimist": "~1.2.5", + "object-inspect": "~1.7.0", + "resolve": "~1.17.0", + "resumer": "~0.0.0", + "string.prototype.trim": "~1.2.1", + "through": "~2.3.8" + }, + "dependencies": { + "is-regex": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.5.tgz", + "integrity": "sha512-vlKW17SNq44owv5AQR3Cq0bQPEb8+kF3UKZ2fiZNOWtztYE5i0CzCZxFDwO58qAOWtxdBRVO/V5Qin1wjCqFYQ==", + "requires": { + "has": "^1.0.3" + } + }, + "object-inspect": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", + "integrity": "sha512-a7pEHdh1xKIAgTySUGgLMx/xwDZskN1Ud6egYYN3EdRW4ZMPNEDUTF+hwy2LUC+Bl+SyLXANnwz/jyh/qutKUw==" + } + } + }, "terser": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/terser/-/terser-5.3.2.tgz", @@ -10688,8 +11772,7 @@ "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", - "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", - "dev": true + "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=" }, "through2": { "version": "3.0.2", @@ -10707,6 +11790,16 @@ "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "dev": true }, + "tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==" + }, + "tinyqueue": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/tinyqueue/-/tinyqueue-2.0.3.tgz", + "integrity": "sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==" + }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -10815,6 +11908,11 @@ "tslib": "^1.8.1" } }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==" + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -10830,6 +11928,11 @@ "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", "dev": true }, + "typedarray": { + "version": "0.0.6", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", + "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=" + }, "typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", @@ -10850,6 +11953,50 @@ "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" }, + "uglify-js": { + "version": "2.8.29", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", + "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", + "requires": { + "source-map": "~0.5.1", + "uglify-to-browserify": "~1.0.0", + "yargs": "~3.10.0" + }, + "dependencies": { + "camelcase": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", + "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" + }, + "cliui": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", + "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", + "requires": { + "center-align": "^0.1.1", + "right-align": "^0.1.1", + "wordwrap": "0.0.2" + } + }, + "yargs": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", + "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", + "requires": { + "camelcase": "^1.0.2", + "cliui": "^2.1.0", + "decamelize": "^1.0.0", + "window-size": "0.1.0" + } + } + } + }, + "uglify-to-browserify": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", + "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", + "optional": true + }, "unherit": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", @@ -10882,12 +12029,46 @@ "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", "dev": true }, + "unicode-properties": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/unicode-properties/-/unicode-properties-1.3.1.tgz", + "integrity": "sha512-nIV3Tf3LcUEZttY/2g4ZJtGXhWwSkuLL+rCu0DIAMbjyVPj+8j5gNVz4T/sVbnQybIsd5SFGkPKg/756OY6jlA==", + "requires": { + "base64-js": "^1.3.0", + "unicode-trie": "^2.0.0" + }, + "dependencies": { + "base64-js": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", + "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==" + }, + "unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "requires": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + } + } + }, "unicode-property-aliases-ecmascript": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", "dev": true }, + "unicode-trie": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-1.0.0.tgz", + "integrity": "sha512-v5raLKsobbFbWLMoX9+bChts/VhPPj3XpkNr/HbqkirXR1DPk8eo9IYKyvk0MQZFkaoRsFj2Rmaqgi2rfAZYtA==", + "requires": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "unified": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.0.tgz", @@ -11070,8 +12251,7 @@ "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", - "dev": true + "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, "util.promisify": { "version": "1.0.1", @@ -11107,6 +12287,16 @@ "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", "dev": true }, + "venn.js": { + "version": "0.2.20", + "resolved": "https://registry.npmjs.org/venn.js/-/venn.js-0.2.20.tgz", + "integrity": "sha512-bb5SYq/wamY9fvcuErb9a0FJkgIFHJjkLZWonQ+DoKKuDX3WPH2B4ouI1ce4K2iejBklQy6r1ly8nOGIyOCO6w==", + "requires": { + "d3-selection": "^1.0.2", + "d3-transition": "^1.0.1", + "fmin": "0.0.2" + } + }, "vfile": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.0.tgz", @@ -11170,11 +12360,30 @@ "integrity": "sha1-Zws6+8VS4LVd9rd4DKdGFfI60cs=", "dev": true }, + "window-size": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", + "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + }, + "wmf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wmf/-/wmf-1.0.2.tgz", + "integrity": "sha512-/p9K7bEh0Dj6WbXg4JG0xvLQmIadrner1bi45VMJTfnbVHsc7yIajZyoSoK60/dtVBs12Fm6WkUI5/3WAVsNMw==" + }, + "word": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/word/-/word-0.3.0.tgz", + "integrity": "sha512-OELeY0Q61OXpdUfTp+oweA/vtLVg5VDOXh+3he3PNzLGG/y0oylSOC1xRVj0+l4vQ3tj/bB1HVHv1ocXkQceFA==" + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", - "dev": true + "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==" + }, + "wordwrap": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", + "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" }, "wrap-ansi": { "version": "6.2.0", @@ -11249,8 +12458,7 @@ "wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" }, "write": { "version": "1.0.3", @@ -11273,11 +12481,41 @@ "typedarray-to-buffer": "^3.1.5" } }, + "xlsx": { + "version": "0.16.7", + "resolved": "https://registry.npmjs.org/xlsx/-/xlsx-0.16.7.tgz", + "integrity": "sha512-Xc4NRjci2Grbh9NDk/XoaWycJurxEug1wwn0aJCmB0NvIMyQuHYq2muWLWGidYNZPf94aUbqm6K8Fbjd7gKTZg==", + "requires": { + "adler-32": "~1.2.0", + "cfb": "^1.1.4", + "codepage": "~1.14.0", + "commander": "~2.17.1", + "crc-32": "~1.2.0", + "exit-on-epipe": "~1.0.1", + "ssf": "~0.11.2", + "wmf": "~1.0.1", + "word": "~0.3.0" + }, + "dependencies": { + "commander": { + "version": "2.17.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.17.1.tgz", + "integrity": "sha512-wPMUt6FnH2yzG95SA6mzjQOEKUU3aLaDEmzs1ti+1E9h+CsrZghRlqEM/EJ4KscsQVG8uNN4uVreUeT8+drlgg==" + } + } + }, + "xmldoc": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/xmldoc/-/xmldoc-1.1.2.tgz", + "integrity": "sha512-ruPC/fyPNck2BD1dpz0AZZyrEwMOrWTO5lDdIXS91rs3wtm4j+T8Rp2o+zoOYkkAxJTZRPOSnOGei1egoRmKMQ==", + "requires": { + "sax": "^1.2.1" + } + }, "xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" }, "y18n": { "version": "4.0.0", diff --git a/package.json b/package.json index a131f985cc87b63e168fa6ae0944c43568649c88..4adcadf9241a2bfe5150eb5a89819a53275c31d0 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,8 @@ "commit": "git-cz" }, "dependencies": { + "@amcharts/amcharts4": "^4.9.37", + "@amcharts/amcharts4-geodata": "^4.1.17", "@popperjs/core": "^2.5.3", "choices.js": "^9.0.1", "prosemirror-example-setup": "^1.1.2", diff --git a/public/favicon.ico b/public/favicon.ico index 3a7011d31145b36525ab60c8ed7dc0dbf07d7303..a55e7ac72447c43fa31a4a9ac72f1e35545ca29e 100644 Binary files a/public/favicon.ico and b/public/favicon.ico differ