├── docker-compose.yml ├── .gitignore ├── src ├── CS50 │ ├── Database │ │ ├── MySQL.php │ │ └── PostgreSQL.php │ ├── Database.php │ └── ID.php └── CS50.php ├── composer.json ├── LICENSE ├── README.md ├── .github └── workflows │ └── main.yml └── Makefile /docker-compose.yml: -------------------------------------------------------------------------------- 1 | cli: 2 | volumes: 3 | - ./:/root 4 | image: cs50/cli:ubuntu 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .* 2 | !.github 3 | !.gitignore 4 | build/ 5 | debian/changelog 6 | debian/*.log 7 | -------------------------------------------------------------------------------- /src/CS50/Database/MySQL.php: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/CS50/Database/PostgreSQL.php: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "authors": [ 3 | { 4 | "email": "sysadmins@cs50.harvard.edu", 5 | "name": "CS50" 6 | } 7 | ], 8 | "autoload": { 9 | "psr-4" : { 10 | "CS50\\" : "src/CS50" 11 | } 12 | }, 13 | "description": "CS50 library for PHP", 14 | "homepage": "https://github.com/cs50/php-cs50", 15 | "keywords": ["cs50"], 16 | "license": "MIT", 17 | "name": "cs50/php", 18 | "repositories": [{ 19 | "type": "vcs", 20 | "url": "https://github.com/cs50/php-cs50" 21 | }], 22 | "require": { 23 | "league/oauth2-client": "^1.1", 24 | "league/uri": "^4.0", 25 | "php": ">=5.4", 26 | "ramsey/uuid": "^3.1" 27 | }, 28 | "support": { 29 | "issues": "https://github.com/cs50/php-cs50/issues", 30 | "source": "https://github.com/cs50/php-cs50" 31 | }, 32 | "type": "library" 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2013-2022 CS50 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CS50 Library for PHP 2 | 3 | ## Development 4 | 5 | Requires [Docker Toolbox](https://www.docker.com/products/docker-toolbox). 6 | 7 | docker-compose run cli # runs CS50 CLI 8 | 9 | ## Installation 10 | 11 | ### Ubuntu: 12 | 13 | ``` 14 | $ curl -s https://packagecloud.io/install/repositories/cs50/repo/script.deb.sh | sudo bash 15 | $ sudo apt-get install php-cs50 16 | ``` 17 | 18 | ### Fedora 19 | 20 | ``` 21 | $ curl -s https://packagecloud.io/install/repositories/cs50/repo/script.rpm.sh | sudo bash 22 | $ sudo yum install php-cs50 23 | ``` 24 | 25 | ### From Source 26 | 27 | 1. Download the latest release from https://github.com/cs50/php-cs50/releases 28 | 1. Extract `php-cs50*` 29 | 1. cd `php-cs50` 30 | 1. `make install # may require sudo` 31 | 32 | By default, we install to `/usr/local/share/php`. If you'd like to change the installation location, run `DESTDIR=/path/to/install make install` as desired. 33 | 34 | ## Usage 35 | 36 | // assumes CS50.php is in include_path 37 | require("CS50.php"); 38 | 39 | ... 40 | 41 | $c = CS50\get_char(); 42 | $f = CS50\get_float(); 43 | $i = CS50\get_int(); 44 | $s = CS50\get_string(); 45 | 46 | ## TODO 47 | 48 | * Decide whether to add `CS50.eprintf`. 49 | * Add tests. 50 | * Review `ID.php`, `Database.php`, etc. 51 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | on: push 2 | jobs: 3 | build-and-deploy: 4 | runs-on: ubuntu-latest 5 | steps: 6 | - uses: actions/checkout@v4 7 | - name: Install dependencies 8 | run: | 9 | sudo apt-get install -y rpm 10 | sudo gem install fpm package_cloud 11 | 12 | - name: Build 13 | run: | 14 | make deb 15 | make rpm 16 | 17 | - name: Deploy 18 | if: ${{ github.ref == 'refs/heads/main' }} 19 | run: | 20 | PACKAGECLOUD_REPO="cs50/repo" 21 | 22 | # Deploy deb to ubuntu repos 23 | UBUNTU_REPOS=( xenial yakkety zesty artful bionic disco eoan focal groovy ) 24 | for repo in "${UBUNTU_REPOS[@]}"; do 25 | package_cloud push "$PACKAGECLOUD_REPO"/ubuntu/"$repo" build/deb/*.deb 26 | done 27 | 28 | # Deploy rpm to fedora repos 29 | for repo in $(seq 28 32); do 30 | package_cloud push "$PACKAGECLOUD_REPO"/fedora/"$repo" build/rpm/*.rpm 31 | done 32 | env: 33 | PACKAGECLOUD_TOKEN: ${{ secrets.PACKAGECLOUD_TOKEN }} 34 | 35 | - name: Extract version from Makefile 36 | id: make_version 37 | run: | 38 | echo ::set-output name=version::$(make version) 39 | 40 | - name: Create Release 41 | if: ${{ github.ref == 'refs/heads/main' }} 42 | uses: actions/github-script@v7 43 | with: 44 | github-token: ${{ github.token }} 45 | script: | 46 | github.rest.repos.createRelease({ 47 | owner: context.repo.owner, 48 | repo: context.repo.repo, 49 | tag_name: "v${{ steps.make_version.outputs.version }}", 50 | tag_commitish: "${{ github.sha }}" 51 | }) 52 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | DESTDIR ?= /usr/local/share/php 2 | VERSION = 6.0.1 3 | 4 | SRC := $(wildcard src/**/*.php) 5 | 6 | .PHONY: clean 7 | clean: 8 | rm -rf build 9 | 10 | .PHONY: deb 11 | deb: $(SRC) 12 | rm -rf build/deb 13 | mkdir -p build/deb/php-cs50/usr/share/php 14 | cp -r src/* build/deb/php-cs50/usr/share/php 15 | 16 | fpm \ 17 | --category php \ 18 | --conflicts library50-php \ 19 | --chdir build/deb/php-cs50 \ 20 | --deb-priority optional \ 21 | --description "CS50 library for PHP" \ 22 | --input-type dir \ 23 | --license "MIT" \ 24 | --maintainer "CS50 " \ 25 | --name php-cs50 \ 26 | --output-type deb \ 27 | --package build/deb \ 28 | --provides library50-php \ 29 | --provides php-cs50 \ 30 | --replaces library50-php \ 31 | --replaces php-cs50 \ 32 | --url https://github.com/cs50/php-cs50 \ 33 | --vendor CS50 \ 34 | --version $(VERSION) \ 35 | . 36 | 37 | rm -rf build/deb/php-cs50 38 | 39 | .PHONY: rpm 40 | rpm: $(SRC) 41 | rm -rf build/rpm 42 | mkdir -p build/rpm/php-cs50/usr/share/php 43 | cp -r src/* build/rpm/php-cs50/usr/share/php 44 | 45 | fpm \ 46 | --category php \ 47 | --chdir build/rpm/php-cs50 \ 48 | --description "CS50 library for PHP" \ 49 | --input-type dir \ 50 | --license "MIT" \ 51 | --maintainer "CS50 " \ 52 | --name php-cs50 \ 53 | --output-type rpm \ 54 | --package build/rpm \ 55 | --provides php-cs50 \ 56 | --url https://github.com/cs50/php-cs50 \ 57 | --vendor CS50 \ 58 | --version $(VERSION) \ 59 | . 60 | 61 | rm -rf build/rpm/php-cs50 62 | 63 | .PHONY: install 64 | install: 65 | mkdir -p $(DESTDIR) 66 | cp -r src/* $(DESTDIR) 67 | 68 | .PHONY: version 69 | version: 70 | @echo $(VERSION) 71 | -------------------------------------------------------------------------------- /src/CS50.php: -------------------------------------------------------------------------------- 1 | 93 | -------------------------------------------------------------------------------- /src/CS50/Database.php: -------------------------------------------------------------------------------- 1 | handle = new PDO($dsn, $username, $password); 23 | } 24 | catch (Exception $e) 25 | { 26 | trigger_error($e->getMessage(), E_USER_ERROR); 27 | } 28 | } 29 | 30 | /** 31 | * Executes SQL statement after substituting (escaped) values for positional placeholders, if any. 32 | * If query is SELECT, returns array of rows; if query is DELETE, INSERT, or UPDATE, 33 | * returns number of rows affected. 34 | * 35 | * @param string $sql 36 | * @param mixed [parameter ...] 37 | * 38 | * @return array|number 39 | */ 40 | public function query(/* $sql [, ... ] */) 41 | { 42 | // SQL statement 43 | $sql = func_get_arg(0); 44 | 45 | // parameters, if any 46 | $parameters = array_slice(func_get_args(), 1); 47 | 48 | // ensure number of placeholders matches number of values 49 | // http://stackoverflow.com/a/22273749 50 | // https://eval.in/116177 51 | $pattern = " 52 | /(?: 53 | '[^'\\\\]*(?:(?:\\\\.|'')[^'\\\\]*)*' 54 | | \"[^\"\\\\]*(?:(?:\\\\.|\"\")[^\"\\\\]*)*\" 55 | | `[^`\\\\]*(?:(?:\\\\.|``)[^`\\\\]*)*` 56 | )(*SKIP)(*F)| \? 57 | /x 58 | "; 59 | preg_match_all($pattern, $sql, $matches); 60 | if (count($matches[0]) < count($parameters)) 61 | { 62 | trigger_error("Too few placeholders in query", E_USER_ERROR); 63 | } 64 | else if (count($matches[0]) > count($parameters)) 65 | { 66 | trigger_error("Too many placeholders in query", E_USER_ERROR); 67 | } 68 | 69 | // replace placeholders with quoted, escaped strings 70 | $patterns = []; 71 | $replacements = []; 72 | for ($i = 0, $n = count($parameters); $i < $n; $i++) 73 | { 74 | array_push($patterns, $pattern); 75 | array_push($replacements, preg_quote($handle->quote($parameters[$i]))); 76 | } 77 | $query = preg_replace($patterns, $replacements, $sql, 1); 78 | 79 | // execute query 80 | $statement = $handle->query($query); 81 | if ($statement === false) 82 | { 83 | trigger_error($handle->errorInfo()[2], E_USER_ERROR); 84 | } 85 | 86 | // if query was SELECT 87 | // http://stackoverflow.com/a/19794473/5156190 88 | if ($statement->columnCount() > 0) 89 | { 90 | // return result set's rows 91 | return $statement->fetchAll(PDO::FETCH_ASSOC); 92 | } 93 | 94 | // if query was DELETE, INSERT, or UPDATE 95 | else 96 | { 97 | // return number of rows affected 98 | return $statement->rowCount(); 99 | } 100 | } 101 | } 102 | 103 | ?> 104 | -------------------------------------------------------------------------------- /src/CS50/ID.php: -------------------------------------------------------------------------------- 1 | provider = new \League\OAuth2\Client\Provider\GenericProvider([ 21 | "clientId" => $client_id, 22 | "clientSecret" => $client_secret, 23 | "redirectUri" => $redirect_uri, 24 | "scopes" => $scope, 25 | "urlAccessToken" => "https://id.cs50.net/token", 26 | "urlAuthorize" => "https://id.cs50.net/authorize", 27 | "urlResourceOwnerDetails" => "https://id.cs50.net/userinfo" 28 | ]); 29 | } 30 | 31 | /** 32 | * Authenticates user via CS50 ID. If user is returning from CS50 ID, 33 | * returns associative array of user's claims, else redirects to CS50 ID 34 | * for authentication. 35 | * 36 | * @param string client_id 37 | * @param string client_secret 38 | * @param string scope 39 | * 40 | * @return array claims 41 | */ 42 | public static function authenticate($client_id, $client_secret, $scope = "openid profile") 43 | { 44 | // validate scope 45 | // https://tools.ietf.org/html/rfc6749#appendix-A.4 46 | if (!preg_match("/^[\x{21}\x{23}-\x{5B}\x{5D}-\x{7E}]([ \x{21}\x{23}-\x{5B}\x{5D}-\x{7E}])*$/", $scope)) 47 | { 48 | trigger_error("invalid scope", E_USER_ERROR); 49 | } 50 | 51 | // redirection URI 52 | try 53 | { 54 | // sans username and password (and fragment) 55 | $uri = \League\Uri\Schemes\Http::createFromServer($_SERVER)->withUserInfo(""); 56 | 57 | // sans code and state (which are reserved by OAuth2) 58 | $modifier = new \League\Uri\Modifiers\RemoveQueryKeys(["code", "state"]); 59 | $redirect_uri = $modifier->__invoke($uri)->__toString(); 60 | } 61 | catch (\Exception $e) 62 | { 63 | trigger_error("unable to infer redirect_uri", E_USER_ERROR); 64 | } 65 | 66 | // configure client 67 | $id = new ID($client_id, $client_secret, $redirect_uri, $scope); 68 | 69 | // if user is returning from CS50 ID, return claims 70 | if (isset($_GET["code"], $_GET["state"])) 71 | { 72 | return $id->getUser(); 73 | } 74 | 75 | // redirect to CS50 ID 76 | header("Location: " . $id->getLoginUrl()); 77 | exit; 78 | } 79 | 80 | /** 81 | * Returns URL to which user should be redirected for authentication via CS50 ID. 82 | * 83 | * @return string URL 84 | */ 85 | public function getLoginUrl() 86 | { 87 | // deprecate old usage 88 | if (func_num_args() !== 0) 89 | { 90 | trigger_error("too many arguments", E_USER_ERROR); 91 | } 92 | 93 | // return OP Endpoint URL with CSRF protection 94 | // https://tools.ietf.org/html/rfc6749#section-10.12 95 | @session_start(); 96 | return $this->provider->getAuthorizationUrl(["state" => hash("sha256", session_id())]); 97 | } 98 | 99 | /** 100 | * Gets claims from an Authorization Response. 101 | * 102 | * @return array|false claims 103 | */ 104 | public function getUser() 105 | { 106 | // deprecate old usage 107 | if (func_num_args() !== 0) 108 | { 109 | trigger_error("too many arguments", E_USER_ERROR); 110 | } 111 | 112 | // if returning from CS50 ID 113 | if (!isset($_GET["code"])) 114 | { 115 | trigger_error("missing code", E_USER_ERROR); 116 | } 117 | 118 | // validate state to prevent CSRF 119 | // http://www.twobotechnologies.com/blog/2014/02/importance-of-state-in-oauth2.html 120 | if (!isset($_GET["state"])) 121 | { 122 | trigger_error("missing state in request", E_USER_WARNING); 123 | return false; 124 | } 125 | @session_start(); 126 | if ($_GET["state"] !== hash("sha256", session_id())) 127 | { 128 | trigger_error("invalid state", E_USER_WARNING); 129 | return false; 130 | } 131 | 132 | // exchange code for token 133 | try 134 | { 135 | $token = $this->provider->getAccessToken("authorization_code", ["code" => $_GET["code"]]); 136 | } 137 | catch (\Exception $e) 138 | { 139 | trigger_error($e->getMessage(), E_USER_NOTICE); 140 | return false; 141 | } 142 | 143 | // get UserInfo with token 144 | try 145 | { 146 | $owner = $this->provider->getResourceOwner($token); 147 | return $owner->toArray(); 148 | } 149 | catch (\Exception $e) 150 | { 151 | trigger_error($e->getMessage(), E_USER_NOTICE); 152 | return false; 153 | } 154 | } 155 | } 156 | 157 | ?> 158 | --------------------------------------------------------------------------------