Giter Club home page Giter Club logo

zammad-api-client-php's People

Contributors

jacques avatar jaytaph avatar jepf avatar lxlang avatar mantas avatar martini avatar mgruner avatar mrgeneration avatar mtexx avatar obuchmann avatar pebosi avatar rocramer avatar spice-king avatar zkrapavickas 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

Watchers

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

zammad-api-client-php's Issues

Add text_modules to resources

Hi there

We would appreciate it if you could add the text_modules to the resources in order to support them nicely via the api client.

BR wucherpfennig

Small improvement of the documentation

Just a small additional information for section "Fetching content of ticket article attachments",
where is mentioned:

$attachment_content = $ticket_article->getAttachmentContent(23);

Additonal information:
"The output of $ticket_article->getAttachmentContent() is a GuzzleHttp\Psr7\Stream Object. To access the content ("body") of this object, use

$guzzle_contents = $attachment_content->getContents();
"

It took me a while to figure out how to access the "body" or content of the GuzzleHttp\Psr7\Stream object for further processing. Maybe this addition to the documentation can save someone time.

By the way thank you for the great PHP client for Zammad API.

Search method on Single Select field doesn't return results anymore

Since a few days the PHP-API doesn't return any result on the following search (a Single Select Object field):

$tickets = $client->resource(ResourceType::TICKET)->search('FIELD_NAME:KEY');

The $tickets-array is always empty. Regular full-text searches still work.
Can anyone suggest a new syntax or help out?

How to access ticket messages

Hi, how do I access ticket messages/updates? There doesn't seem to be a demonstration of this in the examples folder.

Thank you very much for your time

Zammad 3.5/3.4 API incompatibilities

Hi @jepf - we had to change the following API endpoints because of a security issue:

  • GET /api/v1/tags/add -> POST /api/v1/tags/add
  • GET /api/v1/tags/remove -> DELETE /api/v1/tags/remove
  • GET /api/v1/links/add -> POST /api/v1/links/add
  • GET /api/v1/links/remove -> DELETE /api/v1/links/remove
  • GET /api/v1/ticket_merge/:id_source/:id_target -> PUT /api/v1/ticket_merge/:id_source/:id_target

Can you please check this and apply the required changes?

Support PHP 8.x

Currently the library only supports PHP 7.4
As of 28 November 2022 that version is EOL.

To make the Library available to new Projects using PHP 8.x at least a bump of the version within the composer.json PHP Version constraint would be needed.

Also the Github Workflow would need adjustments, I guess, to let all tests run within the max. supported version (PHP8.2 would be a good target at the moment as its the most recent stable version) but also with the min. supported Version (PHP 7.2) -- Question would be if a BC Breaking Major/Minor Version bump should be done and dropping PHP 7.x support all together enabling the Library to move forward with modern PHP Features. (But strongly optional, it would already be great if it installs on PHP8.x and is usable in modern/new applications)

Guzzle 7

Please add support for Guzzle 7 to allow laravel 8

HTTP headers casing

Hi everyone,
we are having some problems with the case of the headers.

The Client/Response class has to decide if the body has to be parsed as json:

public function __construct(
        $status_code,
        $reason_phrase,
        $body,
        array  $headers = []
    )
    {
        $this->status_code   = intval($status_code);
        $this->reason_phrase = $reason_phrase;
        $this->body          = $body;
        $this->headers       = $headers;

        // Store decoded JSON data, if present
        if (
            !empty( $this->headers['Content-Type'] )
            && mb_strpos( $this->headers['Content-Type'][0], 'application/json;' ) !== false
        ) {
            $this->data = json_decode( $this->body, true );

            if ( !empty( $this->data['error'] ) ) {
                $this->error = $this->data['error'];
            }
        }
    }

now our problem is that the response has all headers in lowercase, so the body is never parsed and the data field is never fulfilled. The solution looks easy but before I wanted to ask if no one had to solve this.

Search for user – empty return

Hello here,

I have exactly the same issue than @MrGeneration and @heini23 (#25).

If I follow his example:

  • The client is calling "/api/v1/users/search?expand=1&query=mustermann%40musterfirma.de
  • Zammad API return: []

But, if I add "&sort_by=created_at" to the URL called by the client, Zammad return the user.

Tested with self-hosted Zammad 3.2 and 3.3.

To temporary fix this issue, I added the following line into src/Resource/AbstractResource.php search function:
$url_parameters['sort_by'] = 'created_at';

Maybe adding a param to this function to sort may be a good idea?

Thank you for your great work on Zammad!

Rich Text through API

The Rich Text that is input into Zammad itself is rendered as html when accessed through the API.
When I try to submit some "formatted" text to a ticket article on ticket creation it is not working.

I understand that it would be risky to accept html as input for the article body but would it be possible to allow some sort of formatting like <b> or <i> tags or even a completely different format.

"on_behalf_of_user" never gets initialized, which leads to conflicts

Creating a ticket via:

$ticket = $client->resource(ResourceType::TICKET);
$ticket_data = [
      'title' => 'my-title',
      'group' => 'GroupA',
      'customer_id' => 'guess:[email protected]',
      'article' => [
        'body' => 'Test123',
      ],
    ];
$ticket->setValues($ticket_data);
$ticket->save();

will lead to:

Deprecated function: mb_strlen(): Passing null to parameter #1 ($string) of type string is deprecated in ZammadAPIClient\Client->request() (line 75 of /var/www/html/vendor/zammad/zammad-api-client-php/src/Client.php
Using PHP 8.1.

The problem is, that in line 75 "mb_strlen($this->on_behalf_of_user)" is called, but this variable is never set, which leads to the deprecation error.

In "Client.php" two functions are defined to handle "on_behalf_of_user":

  • setOnBehalfOfUser()
  • unsetOnBehalfOfUser()

Both functions are never actually used in Code, and "on_behalf_of_user" is neither set in the constructor. So my guess is, that "on_behalf_of_user" is never actually initialized and therefore NULL. Hence, the deprecation error.

Get User ID from email address

Hello! First of all, congratulations for your job, it's very useful to me :)

I'm intending to get the user ID from its mail, but I'm facing a private array. Here is the code:

`$email_address = '[email protected]';
$user_data = [
'login' => $email_address,
'email' => $email_address,
];
$user = $client->resource( ResourceType::USER );
$user->setValues($user_data);
$user->save();
$user_id = $user->getID();

if (is_null($user_id)){
$error = $user->getError();
var_dump($error);
if($error == 'Object already exists!'){
$user_by_email= $client->resource( ResourceType::USER )->search($email_address);
$user_by_email->getValue('id');
}
}`

With this code I'm getting this error:

PHP Fatal error: Uncaught Error: Call to a member function getValue() on array in /home/xxxxxx/git_projects/xxxxx/zammad test/zammadusers.php:30

So I tried accessing the user array by var_dump($user_by_email); on the last line of the code. The problem is that I am encountering "private"arrays. How can I get the user ID?

array(1) { [0]=> object(ZammadAPIClient\Resource\User)#29 (4) { ["client":"ZammadAPIClient\Resource\AbstractResource":private]=> object(ZammadAPIClient\Client)#2 (2) { ........... } ["remote_data":"ZammadAPIClient\Resource\AbstractResource":private]=> array(42) { ["id"]=>3, [............... }

Thanks in advance! :)

Exception in src/Client.php at line 7

Hi!

I get an error in the Client.php at line 7.

  • at Client ->request ('GET', 'users?expand=1', array('expand' => true))
    in vendor/zammad/zammad-api-client-php/src/Client.php at line 94 +
  • at Client ->get ('users', array('expand' => true))
    in vendor/zammad/zammad-api-client-php/src/Resource/AbstractResource.php at line 318 +

Somehow i don't get an response from the HTTP Client?

Greeds from Austria!

  • David

Declaration Zammad Http Client conflicts with Guzzle

Just installed package in lumen 5.6 (php 7.2) project and caught this error.

PHP Fatal error:  Declaration of ZammadAPIClient\HTTPClient::request($method, $uri, array $options = Array) must be compatible with GuzzleHttp\Client::request($method, $uri = '', array $options = Array) in /var/www/api-data/vendor/zammad/zammad-api-client-php/src/HTTPClient.php on line 143
[2018-05-14 07:26:57] lumen.ERROR: Symfony\Component\Debug\Exception\FatalErrorException: Declaration of ZammadAPIClient\HTTPClient::request($method, $uri, array $options = Array) must be compatible with GuzzleHttp\Client::request($method, $uri = '', array $options = Array) in /var/www/api-data/vendor/zammad/zammad-api-client-php/src/HTTPClient.php:143 Stack trace: #0 /var/www/api-data/vendor/laravel/lumen-framework/src/Concerns/RegistersExceptionHandlers.php(54): Laravel\Lumen\Application->handleShutdown() #1 [internal function]: Laravel\Lumen\Application->Laravel\Lumen\Concerns\{closure}() #2 {main} [] []

In HTTPClient.php line 143:

  Declaration of ZammadAPIClient\HTTPClient::request($method, $uri, array $options = Array) must be compatible with GuzzleHttp\Client::request($method, $uri = '', array $options = Array)

RuntimteException in Client.php

RuntimeException: Unable to create HTTP client request. in ZammadAPIClient\Client->request() (Zeile 87 in ../vendor/zammad/zammad-api-client-php/src/Client.php).

Using Zammad Client in an Drupal 8 enviroment.
Zammad 2.4-dev
Zammad Client 1.2
PHP 7.0

Problem on searching tickets

Hi!

If i use the following:
->resource(ResourceType::TICKET)->search("test");
the Client finds several Tickets.
See:
0 => Ticket {#448 ▼ -client: Client {#648 ▶} -remote_data: array:47 [▼ "id" => 1267 "group_id" => 16 "priority_id" => 2 "state_id" => 1 "organization_id" => 2 "number" => "941267" "title" => "test" "owner_id" => 1 "customer_id" => 53 "note" => null "first_response_at" => null "first_response_escalation_at" => null "first_response_in_min" => null "first_response_diff_in_min" => null "close_at" => null "close_escalation_at" => null "close_in_min" => null "close_diff_in_min" => null "update_escalation_at" => null "update_in_min" => null "update_diff_in_min" =>null`

But if i try to search using the field-syntax it just returns an empty array.
For example:
->resource(ResourceType::TICKET)->search("title:test");

I'm using Zammad 1.3.0. (PHP 7)

Any idea? :-)

Thx for your time!

Best Regards,
David

Cannot access custom ticket attribute

Hello,
I am currently trying to write a php application that uses the data from the Zammad API. To do this I added a custom field to store some extra data. When I manually access the tickets using the URL the field (finished) is present:

{
    "id": 11,
    "group_id": 1,
    "priority_id": 2,
    "state_id": 4,
    ...
    "created_at": "2018-05-04T14:11:40.487Z",
    "updated_at": "2018-05-11T13:04:24.610Z",
    "finished": false
  }

but when I get the data from within the PHP client the new field is not stored in the values.
Am I doing this wrong or are custom fields for some reason not included in the php Client? This kind of confused me because I was not able to find anything that would indicate this behaviour in the sources.

PHP fatal error occassionally

Hi! So occasionally when I am running a script I get the following error message:

PHP Fatal error: Call to a member function getStatusCode() on null in ../zammad/vendor/zammad/zammad-api-client-php/src/Client.php on line 72

The strange thing is that this only occurs sometimes and does not occur everytime I run the same exact script. Do you know what is going on here?

Bug - New Overview doesnt work

Sorry, wrong board. It not an API issue.

We try to configurate an extra overview for spam mails. At the management panel I created a new overview with the name “spam” , made it visible for agents, and selected the conditions as shown in the screenshot.
When I create the new overview, it shows right in the management panel which mails will be in this overview. (I love this feature 😉)
uebersicht_config

After I created the new overview there isn’t a single mail inside. We use the cloud version on zammad.com.
uebersicht_empty

Edit:
sorry,

Search for user – empty return

If we use the api to search for an user, we always get an empty return. Everything else – like searching for tickets - works well.

Example:
https://XXX.zammad.com/api/v1/users/search?query=Max&token=XXX
(the token has all available rights)

We have a user “Max Mustermann”, therefore we would assume to get his user profile as a return. But the return is empty.

Header Return:

  1. Status Code: 200 OK
  2. cache-control: max-age=0, private, must-revalidate
  3. content-encoding: gzip
  4. content-type: application/json; charset=utf-8
  5. csrf-token: XXX
  6. date: Wed, 27 Nov 2019 11:41:51 GMT
  7. etag: W/"XXX"
  8. referrer-policy: strict-origin-when-cross-origin
  9. server: nginx
  10. strict-transport-security: max-age=63072000
  11. x-content-type-options: nosniff
  12. x-download-options: noopen
  13. x-firefox-spdy: h2
  14. x-frame-options: DENY
  15. x-permitted-cross-domain-policies: none
  16. x-request-id: 6137ba5a-b0a0-40c0-ab90-d21e83f9bb77
  17. x-runtime: 0.026655
  18. x-xss-protection: 1; mode=block

Link one ticket with an other

(excuse me, if this is the wrong place to ask for such things, feel free to close the ticket if this isn't right here)

Hi, I'm already working with this client since a few month, but am missing one feature:

Is there a way to see linked tickets and link one ticket with an other through API?
Looks like this is only possible through zammad directly? I can't find anything in the docs, too, so I guess this isn't implemented in zammad.

I'm talking about this feature on right menu in zammad:

image

If I link an ticket with an other, there is no extra info in the object received from the API in any of these tickets.

Thanks!

TicketArticleTest fails because of updated API capabilities (ZAA-2020-24)

Hi @jepf ,

as mentioned earlier: The TicketArticleTest currently fails because we changed the API capabilities when updating a Ticket Article in the scope of ZAA-2020-24. It's no longer possible to update Ticket Article attributes except the Type (internal/external).

The test currently fails with this error:

$ vendor/bin/phpunit
PHPUnit 7.5.20 by Sebastian Bergmann and contributors.
..................................................F............  63 / 107 ( 58%)
............................................                    107 / 107 (100%)
Time: 1.03 minutes, Memory: 6.00 MB
There was 1 failure:
1) ZammadAPIClient\Resource\TicketArticleTest::testUpdate
Changed value of object must match expected one.
Failed asserting that two strings are equal.
--- Expected
+++ Actual
@@ @@
-'Unit test ticket article 1...5fb23c06ca87e6.25564933CHANGED'
+'Unit test ticket article 1...5fb23c06ca87e6.25564933'
/builds/zammad/zammad/zammad-api-client-php/test/ZammadAPIClient/Resource/AbstractBaseTest.php:240
FAILURES!
Tests: 107, Assertions: 691, Failures: 1.

Cannot set created_by_id

I would like to set the created_by_id value with the api to assign a message to an user.

$ticket->setValue("article", array(
    "subject"=>"Test subject",
    "body"=>"test message",
    "created_by_id"=>$userId, // DOESNT WORK
    "updated_by_id"=>$userId // DOESNT WORK
));

Can't sort by last_contact_at or updated_at

I've created a custom resource to search with sort/order, sadly it can't sort by last_contact_at or updated_at.

We use Zammad Cloud.

EDIT: Is there any chance to get this working? I have seen that other users should rebuild the serach indexes, but I don't know how I could do this 😄 .

EDIT2: I use the resoruce with this line of code

$client->resource( \App\Zammad\Resource\TicketCustom::class )->search('last_contact_at:*', null, null, 'last_contact_at', 'asc' );
<?php

namespace App\Zammad\Resource;

use ZammadAPIClient\Exception\AlreadyFetchedObjectException;
use ZammadAPIClient\ResourceType;

class TicketCustom extends \ZammadAPIClient\Resource\Ticket
{
    const URLS = [
        'get'    => 'tickets/{object_id}',
        'all'    => 'tickets',
        'create' => 'tickets',
        'update' => 'tickets/{object_id}',
        'delete' => 'tickets/{object_id}',
        'search' => 'tickets/search',
    ];

    /**
     * Fetches TicketArticle objects of this Ticket object.
     *
     * @return Array of TicketArticle objects   Returns array of ZammadAPIClient\Resource\TicketArticle objects or an empty array.
     */
    public function getTicketArticles()
    {
        $this->clearError();

        if ( empty( $this->getID() ) ) {
            return [];
        }

        $ticket_articles = $this->getClient()->resource( ResourceType::TICKET_ARTICLE )->getForTicket( $this->getID() );
        if ( !is_array($ticket_articles) ) {
            $this->setError( $ticket_articles->getError() );
            return [];
        }

        return $ticket_articles;
    }
    
    /**
     * Fetches object data for searched objects of this type.
     * This method will be used internally and automatically by search() to automate pagination
     * to retrieve all available objects, ignoring the server side limit of fetchable objects.
     *
     * @return mixed                        Returns array of ZammadAPIClient\Resource\... objects
     *                                          or this object on failure.
     */
    private function searchWithoutPagination($search_term)
    {
        $page             = 1;
        $objects_per_page = 100;
        $objects          = [];
        $objects_of_page  = [];

        do {
            $objects_of_page = $this->search( $search_term, $page, $objects_per_page );
            if ( !is_array($objects_of_page) ) {
                return $this;
            }

            $objects = array_merge( $objects, $objects_of_page );

            $is_last_page = count($objects_of_page) < $objects_per_page
                || !count($objects_of_page);

            $page++;
        } while ( !$is_last_page );

        return $objects;
    }

    /**
     * Fetches object data for given search term.
     * Pagination available.
     *
     * @param string  $search_term          Search term.
     * @param integer $page                 Page of objects, optional, if given, $objects_per_page must also be given.
     * @param integer $objects_per_page     Number of objects per page, optional, if given, $page must also be given.
     *
     * @return mixed                        Returns array of ZammadAPIClient\Resource\... objects
     *                                          or this object on failure.
     */
    public function search( $search_term, $page = null, $objects_per_page = null, $sort_by = null, $order_by = null )
    {
        if ( !empty( $this->getValues() ) ) {
            throw new AlreadyFetchedObjectException('Object already contains values, search() not possible, use a new object');
        }

        if ( isset($page) && $page <= 0 ) {
            throw new \RuntimeException('Parameter page must be a > 0');
        }
        if ( isset($objects_per_page) && $objects_per_page <= 0 ) {
            throw new \RuntimeException('Parameter objects_per_page must be a > 0');
        }
        if (
            ( isset($page) && !isset($objects_per_page) )
            || ( !isset($page) && isset($objects_per_page) )
        ) {
            throw new \RuntimeException('Parameters page and objects_per_page must both be given');
        }

        if ( !isset($page) || !isset($objects_per_page) ) {
            return $this->searchWithoutPagination($search_term);
        }

        $url_parameters = [
            'expand' => true,
            'query'  => $search_term,
        ];

        if (isset($sort_by)) {
            $url_parameters['sort_by'] = $sort_by;
        }
        if (isset($order_by)) {
            $url_parameters['order_by'] = $order_by;
        }
        if ( isset($page) && isset($objects_per_page) ) {
            $url_parameters['page']     = $page;
            $url_parameters['per_page'] = $objects_per_page;
        }

        $url      = $this->getURL('search');
        $response = $this->getClient()->get(
            $url,
            $url_parameters
        );

        if ( $response->hasError() ) {
            $this->setError( $response->getError() );
            return $this;
        }

        $this->clearError();

        // Return array of resource objects if no $object_id was given.
        // Note: the resource object (this object) used to execute get() will be left empty in this case.
        $objects = [];
        foreach ( $response->getData() as $object_data ) {
            $object = $this->getClient()->resource( get_class($this) );
            $object->setRemoteData($object_data);
            $objects[] = $object;
        }

        return $objects;
    }
}

Search for specific fields not working

Hey,
in your documentation you write, that an field specific search is possible:

// Field specific search
$tickets = $client->resource( ResourceType::TICKET )->search('title:My Title');

unfortunately zammad always returns an empty result object if I try to search in fields.
Is this feature not yet implemented in the API?

Best regards,
J. Schurse

Guzzle 7

Please add support for Guzzle 7 to allow laravel 8

Call to undefined method GuzzleHttp\Exception\ConnectException::getResponse()

In

$response = $e->getResponse();
the getResponse() method is undefined.

To reproduce it I put in the ZammadAPIClient\Client constructor a wrong URL:

use ZammadAPIClient\Client;
$client = new Client([
    'url' => 'https://wrong.zammad.url', // Wrong URL to my Zammad installation
    ...
]);

In this case $e is an instance of GuzzleHttp\Exception\ConnectException with no getResponse() method.

Wrong documentation for SSL verification switch.

Hello,
I just wanted to make a "quick" proof of concept without having a proper ssl certificate and tried to use the "verifySsl => false" option but I run in the open issue #36.
With a "dirty" workaround Guzzle returned: "Connection refused".

So, I checked the sources und found this:

In the documention is written "verifySsl":

use ZammadAPIClient\Client;
$client = new Client([
    'url'           => 'https://myzammad.com', // URL to your Zammad installation
    'username'      => '[email protected]',  // Username to use for authentication
    'password'      => 'mypassword',           // Password to use for authentication
    // 'timeout'       => 15,                  // Sets timeout for requests, defaults to 5 seconds, 0: no timeout
    // 'debug'         => true,                // Enables debug output
    // 'verifySsl'     => true,                // Enabled SSL verification. You can also give a path to a CA bundle file. Default is true.
]);

But in https://github.com/zammad/zammad-api-client-php/blob/master/src/HTTPClient.php "verify" is checked:

   // Verify ssl
      $verifySsl = true;
      if (
          array_key_exists('verify', $options)
          && (
              is_bool($options['verify'])
              || (
                  is_string($options['verify'])
                  && file_exists($options['verify'])
              )
          )
      ) {
          $verifySsl = $options['verify'];
      }

Cheers,
Tim

Make Guzzles HTTP connect_timeout configurable or set a decent default

In a current project I'm often interacting with the Zammad API. Due to system maintenance or connection issues, my requests sometimes time out. If this happens often enough (especially in a maintenance window), this keeps more and more of my server threads busy, because your PHP API client / Guzzle (curl) waits forever for a connection by default. This leads to resource leaks until PHP max_execution_time kicks in, but that may take quite some time in queued jobs.

Please add an option to make Guzzles 'connect_timeout' configurable:

use ZammadAPIClient\Client;
$client = new Client([
    'url' => 'https://myinstance.zammad.com',
    'http_token' => 'something',
    'timeout' => 15,
    'connect_timeout' => 5, // new option
]);

Make timeout customizable

I'm currently working on a sync tool that uses the api. Due to the high number of users in my system the timeout excedes sometimes.

You should be able to set the timeout when creating a new Client instance.

e.g.

use ZammadAPIClient\Client;
$client = new Client([
    'url'           => 'https://myzammad.com', // URL to your Zammad installation
    'username'      => '[email protected]',  // Username to use for authentication
    'password'      => 'mypassword',           // Password to use for authentication
    'timeout'       => 15,                // Sets timeout to 15 seconds
]);

Ticket Attachment

Hi!

I have a question:
How do i create a TicketAttachment using your Api-Client?

Thanks for your time!

  • David

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.