Git Product home page Git Product logo

shield-oauth's Introduction

Farsi | English

Codeigniter Shield OAuth

PHPCSFixer PHPStan Rector PHPCPD

Logo Shield OAuth

Latest Stable Version Total Downloads Latest Unstable Version License PHP Version Require

Shield OAuth helps you to provide the possibility of login or registering users through the OAuth service. Currently, Shield OAuth supports Google OAuth and GitHub OAuth by default, but it allows you to implement it for any other service, including Yahoo, Facebook, Twitter, LinkedIn, GitLab and ...

In Shield OAuth, it has been considered to be easy to use by developers and the possibility of expansion to connect to other services in the shortest possible time.

Demo Shield OAuth

Requirements

Shield OAuth Documentation

In the documentation, I have explained how to install, configure, and how to create custom class NewOAuth connections to other services. Documentation for Shield OAuth can be found on the docs.

License

This project is licensed under the MIT License - see the LICENSE file for details.

Acknowledgements

Every open-source project depends on it's contributors to be a success. The following users have contributed in one manner or another in making Codeigniter Shield OAuth:

Contributors

shield-oauth's People

Contributors

albertomoricca avatar clsmedia avatar datamweb avatar dependabot[bot] avatar enderlinp avatar jozefrebjak avatar kenjis avatar l-vanel avatar sammyskills avatar webalchemist avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

shield-oauth's Issues

Microsoft OAuth Service

Hi all,

I've added a Microsoft OAuth service and i would like to share with community.

I take this opportunity to request that it be made configurable whether or not new users can be registered

Here are de code :)

config file: App\Config\ShieldOAuthConfig.php

<?php
public array $oauthConfigs = [
    'microsoft' => [
        'client_id'     => 'Get it from Microsoft Entra ID',
        'client_secret' => 'Get it from Microsoft Entra ID',
        'allow_login' => true,
    ],
];

library file: App\Libraries\ShieldOAuth\MicrosoftOAuth.php

<?php

declare(strict_types=1);

namespace App\Libraries\ShieldOAuth;

use Datamweb\ShieldOAuth\Libraries\Basic\AbstractOAuth;
use Exception;
use stdClass;

class MicrosoftOAuth extends AbstractOAuth
{
    private static $API_CODE_URL = 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize';
    private static $API_TOKEN_URL = 'https://login.microsoftonline.com/common/oauth2/token';
    private static $API_USER_INFO_URL = 'https://graph.microsoft.com/v1.0/me';
    private static $APPLICATION_NAME = 'ShieldOAuth';

    protected string $token;
    protected $client;
    protected $config;
    protected string $client_id;
    protected string $client_secret;
    protected string $callback_url;

    public function __construct(string $token = '')
    {
        $this->token  = $token;
        $this->client = \Config\Services::curlrequest();
        $this->config = new \Config\ShieldOAuthConfig();
        $this->callback_url = base_url('oauth/' . $this->config->call_back_route);
        $this->client_id = env('ShieldOAuthConfig.microsoft.client_id', $this->config->oauthConfigs['microsoft']['client_id']);
        $this->client_secret = env('ShieldOAuthConfig.microsoft.client_secret', $this->config->oauthConfigs['microsoft']['client_secret']);
    }
    public function makeGoLink(string $state): string
    {
        try
        {
            if (empty($this->client_id))
            {
                throw new Exception('Microsoft Tenant ID is empty,');
            }

            if (empty($this->client_id))
            {
                throw new Exception('Microsoft Client ID is empty,');
            }

            if (empty($this->client_secret))
            {
                throw new Exception('Microsoft Secret is empty,');
            }

            $query = http_build_query([
                'client_id' => $this->client_id,
                'response_type' => 'code',
                'redirect_uri' => $this->callback_url,
                'response_mode' => 'query',
                'approval_prompt' => 'auto',
                'scope' => 'User.Read profile openid email',
                'state' => $state
            ]);

            $microsoftURL = self::$API_CODE_URL . '?' . $query;

            return $microsoftURL;
        }
        catch (\Throwable $e)
        {
            die($e->getMessage());
        }
    }

    public function fetchAccessTokenWithAuthCode(array $allGet): void
    {
        $client = \Config\Services::curlrequest();

        try
        {
            $response = $client->request('POST', self::$API_TOKEN_URL, [
                'form_params' => [
                    'client_id'     => $this->client_id,
                    'client_secret' => $this->client_secret,
                    'grant_type'    => 'authorization_code',
                    'redirect_uri'  => $this->callback_url,
                    'code'          => $allGet['code']
                ],
                'http_errors' => false,
            ]);

            $response = json_decode($response->getBody());

            if (!empty($response->error))
            {
                throw new Exception($response->error);
            }

            $this->setToken($response->access_token);
        }
        catch (Exception $e)
        {
            die($e->getMessage());
        }
    }

    public function fetchUserInfoWithToken(): object
    {
        try
        {
            $response = $this->client->request('GET', self::$API_USER_INFO_URL, [
                'headers' => [
                    'Authorization' => 'Bearer ' . $this->getToken(),
                ],
                'http_errors' => false,
            ]);
        }
        catch (Exception $e)
        {
            die($e->getMessage());
        }

        $response = json_decode($response->getBody());

        $userInfo = new stdClass();
        $userInfo->username = $response->userPrincipalName;
        $userInfo->email = $response->mail;
        $userInfo->first_name = $response->givenName;
        $userInfo->last_name = $response->surname;
        $userInfo->avatar = '';

        return $userInfo;
    }

    public function setColumnsName(string $nameOfProcess, object $userInfo): array
    {
        if ($nameOfProcess === 'syncingUserInfo')
        {
            $usersColumnsName = [
                $this->config->usersColumnsName['first_name'] => $userInfo->first_name,
                $this->config->usersColumnsName['last_name']  => $userInfo->last_name,
                //$this->config->usersColumnsName['avatar']  => $userInfo->last_name, // No get avantar from request
            ];
        }

        if ($nameOfProcess === 'newUser')
        {

            die('Auto-register users is disabled;');

            // Commented because i not use register new users

            // $usersColumnsName = [
            //     'username'                                    => $userInfo->username,
            //     'email'                                           => $userInfo->email,
            //     'password'                                    => random_string('crypto', 32),
            //     'active'                                          => TRUE,
            //     $this->config->usersColumnsName['first_name'] => $userInfo->first_name,
            //     $this->config->usersColumnsName['last_name']  => $userInfo->last_name,
            //     $this->config->usersColumnsName['avatar']     => $userInfo->avatar,
            // ];
        }

        return $usersColumnsName;
    }
}

Bug: ShieldOAuthModel not initializing parent

PHP Version

8.1.23

CodeIgniter4 Version

4.4.1

Shield Version

dev-develop

Shield OAuth Version?

dev-main

Which operating systems have you tested for this bug?

macOS

Which server did you use?

apache

Database

mysql:latest

Did you add customize OAuth?

no, just Google

What happened?

On oauth/call-back i got an error:
Typed property CodeIgniter\Shield\Models\BaseModel::$tables must not be accessed before initialization
VENDORPATH/codeigniter4/shield/src/Models/UserModel.php at line 196

194 if ($email !== null) {
195 $data = $this->select(
196 sprintf('%1$s.*, %2$s.secret as email, %2$s.secret2 as password_hash', $this->table, $this->tables['identities'])
197 )

i think thats because $this->tables is null which - i believe - is because we're not initializing parent in ShieldOAuthModel

Steps to Reproduce

just regular setup according to repo guide

Expected Output

one should be logged in (or not)

Anything else?

No response

Using email as auth identity is unreliable

PHP Version

8.1

CodeIgniter4 Version

4.3.2

Shield Version

1.0.0-beta.3

Shield OAuth Version?

dev-develop

Which operating systems have you tested for this bug?

Windows

Which server did you use?

apache

Database

MySQL 5.6

Did you add customize OAuth?

YES.
It's not public

What happened?

When signing in with google or github, using the email as authentication identity is fine, but when you add facebook for example, it already breaks, since facebook doesnt always have an email available. A more reliable way would be to use the id, and only pull the email if available

Steps to Reproduce

Use the facebook OAuth available in the discussions, and try signing in with a fb account where you used phone number to sign in

Expected Output

To be able to sign in without unexpected error

Anything else?

I hope i managed to make it as clear as possible, but if not, let me know, and will try to add some sources for explanation

Bug: failure to display the CI debugging tools

CRITICAL - 2022-11-01 15:36:01 --> foreach() argument must be of type array|object, string given
in VENDORPATH\datamweb\shield-oauth\src\Libraries\Basic\ShieldOAuth.php on line 86.
 1 VENDORPATH\datamweb\shield-oauth\src\Libraries\Basic\ShieldOAuth.php(86): CodeIgniter\Debug\Exceptions->errorHandler(2, 'foreach() argument must be of type array|object, string given', 'VENDORPATH\\datamweb\\shield-oauth\\src\\Libraries\\Basic\\ShieldOAuth.php', 86)
 2 VENDORPATH\datamweb\shield-oauth\src\Libraries\Basic\ShieldOAuth.php(111): Datamweb\ShieldOAuth\Libraries\Basic\ShieldOAuth->otherOAuth()

private function otherOAuth(): array
{
$files = $this->oauthClassFiles;
$allAllowedRoutes = '';
foreach ($files as $file) {
// make string github|google and ... from class name
$allAllowedRoutes .= strtolower(str_replace($search = 'OAuth.php', $replace = '|', $subject = $file->getBasename()));
}
$allAllowedRoutes = mb_substr($allAllowedRoutes, 0, -1);
$pieces = explode('|', $allAllowedRoutes);
return array_diff($pieces, ['github', 'google']);
}

Alow to configure shield-oauth inside a module

Hello,

In all my ci4 projects I try not to use the "app" folder, what I do is create a "modules" folder and then one for the module in question.

In this case, I have a module called "Auth", which is located in "./modules/Auth/" and there I have the Shield customizations and it works perfect.

I have also tried to put the Shield OAuth customizations, but I see that it does not work, looking at the code I see that it looks for the files within app/Config.

Is it possible to use Shield OAuth within a module?

Example of my folder esctructure:

image

Bug: Duplicate username error

PHP Version

8.2

CodeIgniter4 Version

4.3.3

Shield Version

latest dev

Shield OAuth Version?

dev-develop

Which operating systems have you tested for this bug?

Linux

Which server did you use?

fpm-fcgi

Database

Mariadb 10.6

Did you add customize OAuth?

No

What happened?

if there is another user with the same username, an error occurs when registering a new account with google login.
duplicate username error

CodeIgniter\Database\Exceptions\DatabaseException #1062

Duplicate entry 'Glad' for key 'username'

SYSTEMPATH/Database/BaseBuilder.php : 2309   —  CodeIgniter\Database\BaseConnection->query ()

2302             array_keys($this->QBSet),
2303             array_values($this->QBSet)
2304         );
2305 
2306         if (! $this->testMode) {
2307             $this->resetWrite();
2308 
2309             $result = $this->db->query($sql, $this->binds, false);
2310 
2311             // Clear our binds so we don't eat up memory
2312             $this->binds = [];
2313 
2314             return $result;
2315         }
2316 

SYSTEMPATH/Model.php : 330   —  CodeIgniter\Database\BaseBuilder->insert ()

323                 );
324             } else {
325                 $sql = 'INSERT INTO ' . $table . ' DEFAULT VALUES';
326             }
327 
328             $result = $this->db->query($sql);
329         } else {
330             $result = $builder->insert();
331         }
332 
333         // If insertion succeeded then save the insert ID
334         if ($result) {
335             $this->insertID = ! $this->useAutoIncrement ? $data[$this->primaryKey] : $this->db->insertID();
336         }
337 

SYSTEMPATH/BaseModel.php : 782   —  CodeIgniter\Model->doInsert ()

775 
776         $eventData = ['data' => $data];
777 
778         if ($this->tempAllowCallbacks) {
779             $eventData = $this->trigger('beforeInsert', $eventData);
780         }
781 
782         $result = $this->doInsert($eventData['data']);
783 
784         $eventData = [
785             'id'     => $this->insertID,
786             'data'   => $eventData['data'],
787             'result' => $result,
788         ];
789 

SYSTEMPATH/Model.php : 730   —  CodeIgniter\BaseModel->insert ()

723                 $this->tempPrimaryKeyValue = $data->{$this->primaryKey};
724             }
725         }
726 
727         $this->escape   = $this->tempData['escape'] ?? [];
728         $this->tempData = [];
729 
730         return parent::insert($data, $returnID);
731     }
732 
733     /**
734      * Updates a single record in the database. If an object is provided,
735      * it will attempt to convert it into an array.
736      *
737      * @param array|int|string|null $id

VENDORPATH/codeigniter4/shield/src/Models/UserModel.php : 253   —  CodeIgniter\Model->insert ()

246      * @throws ValidationException
247      */
248     public function insert($data = null, bool $returnID = true)
249     {
250         // Clone User object for not changing the passed object.
251         $this->tempUser = $data instanceof User ? clone $data : null;
252 
253         $result = parent::insert($data, $returnID);
254 
255         $this->checkQueryReturn($result);
256 
257         return $returnID ? $this->insertID : $result;
258     }
259 
260     /**

SYSTEMPATH/BaseModel.php : 692   —  CodeIgniter\Shield\Models\UserModel->insert ()

685         if (empty($data)) {
686             return true;
687         }
688 
689         if ($this->shouldUpdate($data)) {
690             $response = $this->update($this->getIdValue($data), $data);
691         } else {
692             $response = $this->insert($data, false);
693 
694             if ($response !== false) {
695                 $response = true;
696             }
697         }
698 
699         return $response;

VENDORPATH/codeigniter4/shield/src/Models/UserModel.php : 315   —  CodeIgniter\BaseModel->save ()

308      *
309      * @return true if the save is successful
310      *
311      * @throws ValidationException
312      */
313     public function save($data): bool
314     {
315         $result = parent::save($data);
316 
317         $this->checkQueryReturn($result);
318 
319         return true;
320     }
321 
322     /**

VENDORPATH/datamweb/shield-oauth/src/Controllers/OAuthController.php : 88   —  CodeIgniter\Shield\Models\UserModel->save ()

81 
82         if ($this->checkExistenceUser($find) === false) {
83             helper('text');
84             $users = model('ShieldOAuthModel');
85             // new user
86             $entitiesUser = new User($oauthClass->getColumnsName('newUser', $userInfo));
87 
88             $users->save($entitiesUser);
89             $userid = $users->getInsertID();
90             // To get the complete user object with ID, we need to get from the database
91             $user = $users->findById($userid);
92             $users->save($user);
93             // Add to default group
94             $users->addToDefaultGroup($user);
95         }

SYSTEMPATH/CodeIgniter.php : 934   —  Datamweb\ShieldOAuth\Controllers\OAuthController->callBack ()

927     protected function runController($class)
928     {
929         // This is a Web request or PHP CLI request
930         $params = $this->router->params();
931 
932         $output = method_exists($class, '_remap')
933             ? $class->_remap($this->method, ...$params)
934             : $class->{$this->method}(...$params);
935 
936         $this->benchmark->stop('controller');
937 
938         return $output;
939     }
940 
941     /**

SYSTEMPATH/CodeIgniter.php : 499   —  CodeIgniter\CodeIgniter->runController ()

492             if (! method_exists($controller, '_remap') && ! is_callable([$controller, $this->method], false)) {
493                 throw PageNotFoundException::forMethodNotFound($this->method);
494             }
495 
496             // Is there a "post_controller_constructor" event?
497             Events::trigger('post_controller_constructor');
498 
499             $returned = $this->runController($controller);
500         } else {
501             $this->benchmark->stop('controller_constructor');
502             $this->benchmark->stop('controller');
503         }
504 
505         // If $returned is a string, then the controller output something,
506         // probably a view, instead of echoing it directly. Send it along

SYSTEMPATH/CodeIgniter.php : 368   —  CodeIgniter\CodeIgniter->handleRequest ()

361             $this->response->send();
362             $this->callExit(EXIT_SUCCESS);
363 
364             return;
365         }
366 
367         try {
368             return $this->handleRequest($routes, $cacheConfig, $returnResponse);
369         } catch (RedirectException $e) {
370             $logger = Services::logger();
371             $logger->info('REDIRECTED ROUTE at ' . $e->getMessage());
372 
373             // If the route is a 'redirect' route, it throws
374             // the exception with the $to as the message
375             $this->response->redirect(base_url($e->getMessage()), 'auto', $e->getCode());

FCPATH/index.php : 67   —  CodeIgniter\CodeIgniter->run ()

60  *---------------------------------------------------------------
61  * LAUNCH THE APPLICATION
62  *---------------------------------------------------------------
63  * Now that everything is setup, it's time to actually fire
64  * up the engines and make this app do its thang.
65  */
66 
67 $app->run();
68 

Steps to Reproduce

Manually register a user using Shield with the first name of a test gmail user, eg John. Then log in with a test Gmail account whose user is named John. This will generate a duplicate username error.

Expected Output

It was expected that the username would be registered with some suffix to differentiate it from an existing user.

Anything else?

No response

Bug: error when run: php spark shield:model UserModel

PHP Version

8.2.6

CodeIgniter4 Version

4.3.4

Shield Version

dev-develop

Shield OAuth Version?

dev-main

Which operating systems have you tested for this bug?

Windows

Which server did you use?

cli

Database

MySQL 8.0.33

Did you add customize OAuth?

No

What happened?

D:\www\mobileproxy>php spark shield:model UserModel

CodeIgniter v4.3.5 Command Line Tool - Server Time: 2023-05-23 01:15:15 UTC+00:00


[ErrorException]

foreach() argument must be of type array|object, string given

at VENDORPATH\datamweb\shield-oauth\src\Libraries\Basic\ShieldOAuth.php:86

Backtrace:
  1    VENDORPATH\datamweb\shield-oauth\src\Libraries\Basic\ShieldOAuth.php:86
       CodeIgniter\Debug\Exceptions()->errorHandler(2, 'foreach() argument must be of type array|object, string given', 'D:\\www\\mobileproxy\\vendor\\datamweb\\shield-oauth\\src\\Libraries\\Basic\\ShieldOAuth.php', 86)

  2    VENDORPATH\datamweb\shield-oauth\src\Libraries\Basic\ShieldOAuth.php:111
       Datamweb\ShieldOAuth\Libraries\Basic\ShieldOAuth()->otherOAuth()

  3    VENDORPATH\datamweb\shield-oauth\src\Views\Decorators\ShieldOAuth.php:21
       Datamweb\ShieldOAuth\Libraries\Basic\ShieldOAuth()->makeOAuthButton('login')

  4    SYSTEMPATH\View\ViewDecoratorTrait.php:31
       Datamweb\ShieldOAuth\Views\Decorators\ShieldOAuth::decorate('<@php

declare(strict_types=1);

namespace {namespace};

use CodeIgniter\\Shield\\Models\\UserModel as ShieldUserModel;

class {class} extends ShieldUserModel
{
    protected function initialize(): void
    {
        parent::initialize();

        $this->allowedFields = [
            ...$this->allowedFields,

            // \'first_name\',
        ];
    }
}')

  5    SYSTEMPATH\View\View.php:234
       CodeIgniter\View\View()->decorateOutput('<@php

declare(strict_types=1);

namespace {namespace};

use CodeIgniter\\Shield\\Models\\UserModel as ShieldUserModel;

class {class} extends ShieldUserModel
{
    protected function initialize(): void
    {
        parent::initialize();

        $this->allowedFields = [
            ...$this->allowedFields,

            // \'first_name\',
        ];
    }
}')

  6    SYSTEMPATH\Common.php:1184
       CodeIgniter\View\View()->render('CodeIgniter\\Commands\\Generators\\Views\\usermodel.tpl.php', [...], true)

  7    SYSTEMPATH\CLI\GeneratorTrait.php:274
       view('CodeIgniter\\Commands\\Generators\\Views\\usermodel.tpl.php', [], [...])

  8    SYSTEMPATH\CLI\GeneratorTrait.php:292
       CodeIgniter\Shield\Commands\Generators\UserModelGenerator()->renderTemplate([])

  9    SYSTEMPATH\CLI\GeneratorTrait.php:204
       CodeIgniter\Shield\Commands\Generators\UserModelGenerator()->parseTemplate('App\\Models\\UserModel')

 10    SYSTEMPATH\CLI\GeneratorTrait.php:302
       CodeIgniter\Shield\Commands\Generators\UserModelGenerator()->prepare('App\\Models\\UserModel')

 11    SYSTEMPATH\CLI\GeneratorTrait.php:115
       CodeIgniter\Shield\Commands\Generators\UserModelGenerator()->buildContent('App\\Models\\UserModel')

 12    SYSTEMPATH\CLI\GeneratorTrait.php:94
       CodeIgniter\Shield\Commands\Generators\UserModelGenerator()->generateClass([...])

 13    VENDORPATH\codeigniter4\shield\src\Commands\Generators\UserModelGenerator.php:77
       CodeIgniter\Shield\Commands\Generators\UserModelGenerator()->execute([...])

 14    SYSTEMPATH\CLI\Commands.php:65
       CodeIgniter\Shield\Commands\Generators\UserModelGenerator()->run([...])

 15    SYSTEMPATH\CLI\Console.php:37
       CodeIgniter\CLI\Commands()->run('shield:model', [...])

 16    ROOTPATH\spark:97
       CodeIgniter\CLI\Console()->run()

Steps to Reproduce

Open CMD

Expected Output

php spark shield:model UserModel

Anything else?

No response

92 : HTTP/2 stream 0 was not closed cleanly: INTERNAL_ERROR (err 2)

PHP Version

8.2.5

CodeIgniter4 Version

4.3.6

Shield Version

beta

Shield OAuth Version?

dev-main

Which operating systems have you tested for this bug?

Linux

Which server did you use?

apache

Database

5.7.33

Did you add customize OAuth?

NO

What happened?

I'm trying to use google oauth and I got the error:

92 : HTTP/2 stream 0 was not closed cleanly: INTERNAL_ERROR (err 2)

image

Steps to Reproduce

I tried using Google's login button

Expected Output

working

Anything else?

No response

Dev: add code QC tools

Clean code is an important part of programming, so I try to add the following tools to the shield-oauth for extensibility, cleanliness and reliability.
Any person who has the experience of using or solving problems can help in this matter.

  • phpstan
    -- [ ] #19
    -- [ ] add github action
  • psalm
  • phpunit test

Bug: Error when syncingUserInfo

PHP Version

8.3

CodeIgniter4 Version

4.4.8

Shield Version

1.0.2

Shield OAuth Version?

dev-develop

Which operating systems have you tested for this bug?

macOS

Which server did you use?

cli-server (PHP built-in webserver)

Database

PostgreSQL 15.4

Did you add customize OAuth?

No

What happened?

when logging in using google oauth I got the error as follow:

Undefined property: stdClass:: $family_name
Screenshot 2024-04-12 at 17 59 04

Steps to Reproduce

click google-oauth button

Expected Output

because the family name property was not found, perhaps it could be replaced with the given_name property or not include both because it is already represented by the name property

Anything else?

No response

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.