[
  {
    "path": ".gitignore",
    "content": ".idea\r\n/app/config.php\r\nvendor\r\n/config.yaml\r\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: php\n\nphp:\n- '5.6'\n- '7.0'\n- '7.1'\n\nbefore_install:\n- composer install\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2011 Mike Cao <mike@mikecao.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in\nall copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN\nTHE SOFTWARE."
  },
  {
    "path": "README.md",
    "content": "# Ourls\n\n[![Latest Stable Version](https://poser.pugx.org/takashiki/ourls/v/stable)](https://packagist.org/packages/takashiki/ourls)\n[![Total Downloads](https://poser.pugx.org/takashiki/ourls/downloads)](https://packagist.org/packages/takashiki/ourls)\n[![Latest Unstable Version](https://poser.pugx.org/takashiki/ourls/v/unstable)](https://packagist.org/packages/takashiki/ourls)\n[![License](https://poser.pugx.org/takashiki/ourls/license)](https://packagist.org/packages/takashiki/ourls)\n\nOurls是一个基于发号和hashid的短网址服务，灵感来源于知乎上关于短址算法的一个讨论——\n[http://www.zhihu.com/question/29270034](http://www.zhihu.com/question/29270034)。\n\n## 特征/Feature\n\nOurls会根据sha1值来判断原url在数据库中是否已存在，若不存在则新增记录后对记录id进行hash，产生短网址。\n\nOurls会对输入的url进行标准化处理，若为缺少scheme的url，会默认自动加上`http://`，\n并且会对url的query参数进行排序和urlencode等。\n\n## 演示/Demo\n\n[在线演示/Online Demo](http://skyx.in)\n\n## 安装/Install\n\n下载源码后运行`composer install`安装依赖包，或者运行`composer create-project takashiki/ourls`。\n\n然后将urls.sql导入数据库中，将app目录下config.sample.php重命名为config.php并按自己实际情况修改相关配置项。\n\n> git clone and composer install or composer create-project takashiki/ourls\n\n> import urls.sql to your database\n\n> rename app/config.sample.php to app/config.php\n\n> modify the config file according to your situation\n\n### License\n\nOurls is open-sourced software licensed under the\n[MIT license](http://opensource.org/licenses/MIT)\n"
  },
  {
    "path": "app/components/Hash.php",
    "content": "<?php\n\nnamespace app\\components;\n\nuse Hashids\\Hashids;\n\nclass Hash\n{\n    public $hashids;\n\n    public function __construct(array $params)\n    {\n        $this->hashids = new Hashids(\n            $params['salt'],\n            $params['length'],\n            $params['alphabet']\n        );\n    }\n\n    public function encode($id)\n    {\n        return $this->hashids->encode($id);\n    }\n\n    public function decode($hash)\n    {\n        $id = $this->hashids->decode($hash);\n\n        return $id ? $id[0] : false;\n    }\n}\n"
  },
  {
    "path": "app/config.sample.php",
    "content": "<?php\n\nreturn [\n    'debug'    => true,\n    'base_url' => 'YourSiteUrl',\n    'hash'     => [\n        'salt'     => 'SomeRandomKey',\n        'length'   => 5,\n        'alphabet' => 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890',\n    ],\n    'db' => [\n        'database_type' => 'mysql',\n        'database_name' => 'name',\n        'server'        => 'localhost',\n        'username'      => 'your_username',\n        'password'      => 'your_password',\n        'charset'       => 'utf8',\n        'port'          => 3306,\n        'option'        => [\n            PDO::ATTR_CASE => PDO::CASE_NATURAL,\n        ],\n    ],\n    'db_read' => [\n        'database_type' => 'mysql',\n        'database_name' => 'name',\n        'server'        => 'localhost',\n        'username'      => 'your_username',\n        'password'      => 'your_password',\n        'charset'       => 'utf8',\n        'port'          => 3306,\n        'option'        => [\n            PDO::ATTR_CASE => PDO::CASE_NATURAL,\n        ],\n    ],\n    'settings' => [\n        'external_js' => null,\n    ],\n    'proxies' => [\n        '127.0.0.0/8',\n        '10.0.0.0/8',\n        '172.16.0.0/12',\n        '192.168.0.0/16',\n        'fd00::/8',\n    ],\n];\n"
  },
  {
    "path": "app/helpers.php",
    "content": "<?php\n\nuse Etechnika\\IdnaConvert\\IdnaConvert;\n\nif (!function_exists('avg')) {\n    function avg(array $data)\n    {\n        return array_sum($data) / count($data);\n    }\n}\n\nif (!function_exists('url_modify')) {\n    function url_modify($url, $defaultScheme = 'http')\n    {\n        if (parse_url($url, PHP_URL_SCHEME) == null) {\n            $url = $defaultScheme.'://'.trim($url, '/');\n        }\n        $url = (new URL\\Normalizer($url, true, true))->normalize();\n        if (filter_var(IdnaConvert::encodeString($url), FILTER_VALIDATE_URL) === false) {\n            return false;\n        } else {\n            $fragment = parse_url($url, PHP_URL_FRAGMENT);\n\n            return str_replace('#'.$fragment, '#'.urldecode($fragment), $url);\n        }\n    }\n}\n\nif (!function_exists('real_remote_addr')) {\n    function real_remote_addr()\n    {\n        $ip = Flight::request()->ip;\n        $proxy = Flight::request()->proxy_ip;\n        if ('' != $proxy && Flight::get('proxies')->match($ip)) {\n            return $proxy;\n        } else {\n            return $ip;\n        }\n    }\n}\n\n/*\n * Registers a class and set a variable to framework method.\n *\n * @param string $name Method name\n * @param string $class Class name\n * @param array $params Class initialization parameters\n * @param callback $callback Function to call after object instantiation\n * @throws \\Exception If trying to map over a framework method\n */\nFlight::map('instance', function ($name, $class, array $params = [], $callback = null) {\n    Flight::register($name, $class, $params, $callback);\n    Flight::set($name, Flight::{$name}());\n});\n"
  },
  {
    "path": "app/routes.php",
    "content": "<?php\n\nFlight::route('/', function () {\n    Flight::render('index.php');\n});\n\nFlight::route('/shorten', function () {\n    $url = url_modify(Flight::request()->query['url']);\n    if ($url) {\n        if (strpos($url, Flight::get('flight.base_url')) !== false) {\n            Flight::json(['status' => 0, 'msg' => '该地址无法被缩短']);\n        } else {\n            $sha1 = sha1($url);\n            $store = Flight::get('db_read')->select('urls', ['id'], [\n                'sha1' => $sha1,\n            ]);\n            if (!$store) {\n                $id = Flight::get('db')->insert('urls', [\n                    'sha1'      => $sha1,\n                    'url'       => $url,\n                    'create_at' => time(),\n                    'creator'   => ip2long(real_remote_addr()),\n                ]);\n            } else {\n                $id = $store[0]['id'];\n            }\n            $s_url = Flight::get('flight.base_url').Flight::get('hash')->encode($id);\n            Flight::json(['status' => 1, 's_url' => $s_url]);\n        }\n    } else {\n        Flight::json(['status' => 0, 'msg' => '请传入正确的url']);\n    }\n});\n\nFlight::route('/expand', function () {\n    $s_url = Flight::request()->query['s_url'];\n    if ($s_url) {\n        $hash = str_replace(Flight::get('flight.base_url'), '', $s_url);\n        if (!preg_match('/^['.Flight::get('alphabet').']+$/', $hash)) {\n            Flight::json(['status' => 0, 'msg' => '短址不正确']);\n        } else {\n            $id = Flight::get('hash')->decode($hash);\n            if (!$id) {\n                Flight::json(['status' => 0, 'msg' => '短址无法解析']);\n            } else {\n                $store = Flight::get('db_read')->select('urls', ['url'], [\n                    'id' => $id,\n                ]);\n                if (!$store) {\n                    Flight::json(['status' => 0, 'msg' => '地址不存在']);\n                } else {\n                    Flight::json(['status' => 1, 'url' => $store[0]['url']]);\n                }\n            }\n        }\n    }\n});\n\nFlight::route('/@hash', function ($hash) {\n    $id = Flight::get('hash')->decode($hash);\n    if (!$id) {\n        Flight::notFound('短址无法解析');\n    } else {\n        $store = Flight::get('db_read')->select('urls', ['url'], [\n            'id' => $id,\n        ]);\n        if (!$store) {\n            Flight::notFound('地址不存在');\n        } else {\n            Flight::get('db')->update('urls', ['count[+]' => 1], [\n                'id' => $id,\n            ]);\n            Flight::redirect($store[0]['url'], 302);\n        }\n    }\n});\n\nFlight::map('notFound', function ($message) {\n    Flight::response()->status(404)\n        ->header('content-type', 'text/html; charset=utf-8')\n        ->write(\n            '<h1>404 页面未找到</h1>'.\n            \"<h3>{$message}</h3>\".\n            '<p><a href=\"'.Flight::get('flight.base_url').'\">回到首页</a></p>'.\n            str_repeat(' ', 512)\n        )\n        ->send();\n});\n\nFlight::map('error', function (Exception $ex) {\n    $message = Flight::get('flight.log_errors') ? $ex->getTraceAsString() : '出错了';\n    Flight::response()->status(500)\n        ->header('content-type', 'text/html; charset=utf-8')\n        ->write(\n            '<h1>500 服务器内部错误</h1>'.\n            \"<h3>{$message}</h3>\".\n            '<p><a href=\"'.Flight::get('flight.base_url').'\">回到首页</a></p>'.\n            str_repeat(' ', 512)\n        )\n        ->send();\n});\n"
  },
  {
    "path": "app/views/index.php",
    "content": "<!doctype html>\n<html class=\"no-js\">\n<head>\n    <meta charset=\"utf-8\">\n    <meta http-equiv=\"X-UA-Compatible\" content=\"IE=edge\">\n    <meta name=\"description\" content=\"\">\n    <meta name=\"keywords\" content=\"\">\n    <meta name=\"viewport\"\n          content=\"width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no\">\n    <title>Ourls</title>\n\n    <!-- Set render engine for 360 browser -->\n    <meta name=\"renderer\" content=\"webkit\">\n\n    <!-- No Baidu Siteapp-->\n    <meta http-equiv=\"Cache-Control\" content=\"no-siteapp\"/>\n\n    <!-- Add to homescreen for Chrome on Android -->\n    <meta name=\"mobile-web-app-capable\" content=\"yes\">\n\n    <!-- Add to homescreen for Safari on iOS -->\n    <meta name=\"apple-mobile-web-app-capable\" content=\"yes\">\n    <meta name=\"apple-mobile-web-app-status-bar-style\" content=\"black\">\n    <meta name=\"apple-mobile-web-app-title\" content=\"Amaze UI\"/>\n\n    <meta name=\"msapplication-TileColor\" content=\"#0e90d2\">\n\n    <link href=\"//cdn.bootcss.com/amazeui/2.5.2/css/amazeui.min.css\" rel=\"stylesheet\">\n    <link rel=\"stylesheet\" href=\"css/app.css\">\n</head>\n<body>\n<a href=\"https://github.com/takashiki/ourls\">\n    <img style=\"position: absolute; top: 0; right: 0; border: 0;\" src=\"https://camo.githubusercontent.com/38ef81f8aca64bb9a64448d0d70f1308ef5341ab/68747470733a2f2f73332e616d617a6f6e6177732e636f6d2f6769746875622f726962626f6e732f666f726b6d655f72696768745f6461726b626c75655f3132313632312e706e67\" alt=\"Fork me on GitHub\" data-canonical-src=\"https://s3.amazonaws.com/github/ribbons/forkme_right_darkblue_121621.png\">\n</a>\n\n<div class=\"header\">\n    <div class=\"am-g\">\n        <h1>Ourls</h1>\n        <p>Url Shorten Service<br>基于发号加hash id的短网址服务</p>\n    </div>\n    <hr>\n</div>\n\n<div class=\"am-g\">\n    <div id=\"content\" class=\"am-u-lg-6 am-u-md-8 am-u-sm-centered\">\n        <form class=\"am-form\">\n            <input type=\"url\" name=\"\" id=\"url\" value=\"\" placeholder=\"请在此填写你要转换的长网址或短址\">\n            <br>\n            <div class=\"am-cf\">\n                <input type=\"button\" id=\"shorten\" value=\"转换短址\" class=\"am-btn am-btn-primary am-btn-sm am-fl\">\n                <input type=\"button\" id=\"expand\" value=\"还原短址\" class=\"am-btn am-btn-default am-btn-sm am-fr\">\n            </div>\n        </form>\n        <div id=\"qrcode\" class=\"am-hide am-center am-img-thumbnail am-img-responsive\" style=\"width: 206px;height: 206px\"></div>\n        <hr>\n        <p>© <?= date('Y') ?> <a href=\"https://github.com/takashiki/ourls\" target=\"_blank\">Ourls</a> . Licensed under MIT license.</p>\n    </div>\n</div>\n\n<!--[if (gte IE 9)|!(IE)]><!-->\n<script src=\"//cdn.bootcss.com/jquery/2.1.4/jquery.min.js\"></script>\n<!--<![endif]-->\n<!--[if lte IE 8 ]>\n<script src=\"http://libs.baidu.com/jquery/1.11.3/jquery.min.js\"></script>\n<script src=\"http://cdn.staticfile.org/modernizr/2.8.3/modernizr.js\"></script>\n<script src=\"http://cdn.amazeui.org/amazeui/2.4.2/js/amazeui.ie8polyfill.min.js\"></script>\n<![endif]-->\n<script src=\"//cdn.bootcss.com/amazeui/2.5.2/js/amazeui.min.js\"></script>\n<script src=\"//cdn.bootcss.com/validator/4.0.5/validator.min.js\"></script>\n<script src=\"//cdn.bootcss.com/jquery.qrcode/1.0/jquery.qrcode.min.js\"></script>\n<script src=\"js/index.js\"></script>\n<?php if (!empty(Flight::get('flight.settings')['external_js'])): ?>\n    <script src=\"<?= Flight::get('flight.settings')['external_js'] ?>\"></script>\n<?php endif ?>\n</body>\n</html>"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"takashiki/ourls\",\n    \"description\": \"A url shorten service system base on hash id\",\n    \"keywords\": [\"url shorten\", \"hashids\"],\n    \"homepage\": \"https://github.com/takashiki/ourls\",\n    \"license\": \"MIT\",\n    \"authors\": [\n        {\n            \"name\": \"takashiki\",\n            \"email\": \"857995137@qq.com\",\n            \"homepage\": \"http://blog.skyx.in/\"\n        }\n    ],\n    \"require\": {\n        \"php\": \">=5.6.4\",\n        \"catfan/medoo\": \"^0.9.8\",\n        \"etechnika/idna-convert\": \"^1.1\",\n        \"glenscott/url-normalizer\": \"*\",\n        \"hashids/hashids\": \"^2.0\",\n        \"mikecao/flight\": \"^1.2\",\n        \"wikimedia/ip-set\": \"1.1.0\"\n    },\n    \"autoload\": {\n        \"psr-4\": {\n            \"app\\\\\": \"app/\"\n        }\n    },\n    \"config\": {\n        \"preferred-install\": \"dist\",\n        \"sort-packages\": true,\n        \"optimize-autoloader\": true\n    }\n}\n"
  },
  {
    "path": "public/.htaccess",
    "content": "RewriteEngine On\nRewriteCond %{REQUEST_FILENAME} !-f\nRewriteCond %{REQUEST_FILENAME} !-d\nRewriteRule ^(.*)$ index.php [QSA,L]"
  },
  {
    "path": "public/css/app.css",
    "content": ".header {\n    text-align: center;\n}\n\n.header h1 {\n    font-size: 200%;\n    color: #333;\n    margin-top: 30px;\n}"
  },
  {
    "path": "public/index.php",
    "content": "<?php\n\nrequire __DIR__.'/../vendor/autoload.php';\n\nrequire __DIR__.'/../app/helpers.php';\n\nrequire __DIR__.'/../app/routes.php';\n\n$config = require __DIR__.'/../app/config.php';\n\nFlight::set('flight.log_errors', $config['debug']);\nFlight::set('flight.base_url', $config['base_url']);\nFlight::set('flight.settings', $config['settings']);\nFlight::set('flight.views.path', __DIR__.'/../app/views');\nFlight::set('alphabet', $config['hash']['alphabet']);\n\nFlight::instance('hash', '\\app\\components\\Hash', [$config['hash']]);\nFlight::instance('db', 'medoo', [$config['db']]);\nFlight::instance('db_read', 'medoo', [$config['db_read']]);\nFlight::instance('proxies', '\\IPSet\\IPSet', [$config['proxies']]);\n\nFlight::start();\n"
  },
  {
    "path": "public/js/index.js",
    "content": "$('#shorten').click(function() {\n    var raw_url = $('#url').val();\n    if (validator.isURL(raw_url)) {\n        var url = encodeURIComponent(raw_url);\n        $.getJSON(\n            'shorten?url=' + url,\n            function (data) {\n                if (data.status == 1) {\n                    $('#url').val(data.s_url);\n                    var qrcode = $('#qrcode');\n                    qrcode.qrcode({\n                        width: 200,\n                        height: 200,\n                        text: data.s_url\n                    });\n                    qrcode.removeClass('am-hide');\n                } else {\n                    alert(data.msg);\n                }\n            }\n        )\n    } else {\n        alert('请输入正确的url');\n    }\n});\n\n$('#expand').click(function() {\n    var s_url = $('#url').val();\n    $.getJSON(\n        'expand?s_url=' + s_url,\n        function(data) {\n            if (data.status == 1) {\n                $('#url').val(data.url);\n                var qrcode = $('#qrcode');\n                qrcode.addClass('am-hide');\n                qrcode.html('');\n            } else {\n                alert(data.msg);\n            }\n        }\n    )\n});\n"
  },
  {
    "path": "urls.sql",
    "content": "CREATE TABLE `urls` (\n    `id` INT (11) NOT NULL AUTO_INCREMENT,\n    `sha1` CHAR (40) NOT NULL,\n    `url` VARCHAR (255) NOT NULL,\n    `create_at` INT (11) NOT NULL,\n    `creator` INT (11) unsigned NOT NULL DEFAULT '0',\n    `count` INT (11) NOT NULL DEFAULT '0',\n    `status` TINYINT(1) NOT NULL DEFAULT '1',\n    PRIMARY KEY (`id`),\n    INDEX (`sha1`),\n    INDEX (`create_at`),\n    INDEX (`creator`),\n    INDEX (`count`),\n    INDEX (`status`)\n) ENGINE = INNODB CHARACTER\nSET utf8 COLLATE utf8_unicode_ci;"
  }
]