| RewriteEngine on | |||||
| RewriteBase /textfiles/ | |||||
| # Send requests without parameters to textfiles.php. | |||||
| # RewriteRule ^$ textfiles.php [L] | |||||
| # Send requests for index.php to textfiles.php. | |||||
| # RewriteRule ^index\.php$ textfiles.php [L] | |||||
| # Send requests for index.html to textfiles.php. | |||||
| # RewriteRule ^index\.html$ textfiles.php [L] | |||||
| # Send requests for textfiles.css to textfiles.css. | |||||
| RewriteRule ^textfiles\.css$ textfiles.css [L] | |||||
| # Send requests for textfiles.js to textfiles.js. | |||||
| RewriteRule ^textfiles\.js$ textfiles.js [L] | |||||
| # Send requests for textfiles.php to textfiles.php. | |||||
| RewriteRule ^textfiles\.php$ textfiles.php [L] | |||||
| # Send requests for textfiles.html to textfiles.php. | |||||
| # NOTE: THIS IS NOT A TYPO! | |||||
| RewriteRule ^textfiles\.html$ textfiles.php [L] | |||||
| # Send requests for files that exist to textfiles.php (raw). | |||||
| RewriteCond %{REQUEST_URI} ^\/textfiles\/(.+)/raw$ | |||||
| RewriteCond /file/path/to/your/textfiles/%1 -f | |||||
| RewriteRule ^(.+)/raw$ textfiles.php?f=$1&m=raw [QSA,L] | |||||
| # Send requests for files that exist to textfiles.php. | |||||
| RewriteCond %{REQUEST_URI} ^\/textfiles\/(.+)$ | |||||
| RewriteCond /file/path/to/your/textfiles/%1 -f | |||||
| RewriteRule ^(.+)$ textfiles.php?f=$1 [QSA,L] |
| html { | |||||
| box-sizing: border-box; | |||||
| font-size: 20px; | |||||
| } | |||||
| *, *::before, *::after { | |||||
| box-sizing: inherit; | |||||
| } | |||||
| html, body { | |||||
| margin: 0; | |||||
| padding: 0; | |||||
| } | |||||
| .compensator { | |||||
| width: 15rem; | |||||
| height: 4.5rem; | |||||
| float: right; | |||||
| pointer-events: none; | |||||
| } | |||||
| @media only screen and (max-width: 520px) { | |||||
| .compensator { | |||||
| display: none; | |||||
| } | |||||
| } | |||||
| pre { | |||||
| font-family: Inconsolata, monospace; | |||||
| -moz-tab-size: 4; | |||||
| tab-size: 4; | |||||
| white-space: pre-wrap; | |||||
| overflow-wrap: break-word; | |||||
| padding: 0.5em; | |||||
| } | |||||
| #ui-elements-container { | |||||
| position: fixed; | |||||
| top: 0; | |||||
| left: 0; | |||||
| width: 100%; | |||||
| height: 100vh; | |||||
| pointer-events: none; | |||||
| } | |||||
| #ui-elements-container > * { | |||||
| pointer-events: auto; | |||||
| } | |||||
| #controls { | |||||
| position: absolute; | |||||
| top: 0; | |||||
| right: 0; | |||||
| -webkit-user-select: none; | |||||
| -moz-user-select: none; | |||||
| user-select: none; | |||||
| } | |||||
| @media only screen and (max-width: 520px) { | |||||
| #controls { | |||||
| display: flex; | |||||
| flex-flow: row-reverse; | |||||
| align-items: flex-end; | |||||
| width: 100%; | |||||
| top: unset; | |||||
| bottom: 0; | |||||
| } | |||||
| } | |||||
| #controls .buttons { | |||||
| display: flex; | |||||
| flex-flow: row-reverse wrap; | |||||
| background-color: #fff; | |||||
| padding: 1px; | |||||
| } | |||||
| @media only screen and (max-width: 520px) { | |||||
| #controls .buttons { | |||||
| flex-flow: column; | |||||
| } | |||||
| } | |||||
| #controls .button { | |||||
| background-color: #e00; | |||||
| font-size: 1.5em; | |||||
| display: flex; | |||||
| flex-flow: column; | |||||
| justify-content: center; | |||||
| align-items: center; | |||||
| width: 3.5rem; | |||||
| height: 3.5rem; | |||||
| text-decoration: none; | |||||
| font-family: Font Awesome; | |||||
| color: #fff; | |||||
| padding: 1px 0 0 0; | |||||
| border: none; | |||||
| -webkit-appearance: none; | |||||
| -moz-appearance: none; | |||||
| cursor: pointer; | |||||
| } | |||||
| #controls .button + .button { | |||||
| margin: 0 1px 0 0; | |||||
| } | |||||
| @media only screen and (max-width: 520px) { | |||||
| #controls .button + .button { | |||||
| margin: 1px 0 0 0; | |||||
| } | |||||
| } | |||||
| @media only screen and (hover:hover) and (pointer:fine) { | |||||
| #controls .button:hover { | |||||
| color: gold; | |||||
| } | |||||
| } | |||||
| #controls .button:active { | |||||
| transform: scale(0.975); | |||||
| } | |||||
| #controls .button:focus { | |||||
| outline: none; | |||||
| } | |||||
| #controls .button::after { | |||||
| content: attr(data-label); | |||||
| display: block; | |||||
| font-family: sans-serif; | |||||
| font-size: 0.5rem; | |||||
| font-weight: bold; | |||||
| margin: 6px 0 0 0; | |||||
| text-transform: uppercase; | |||||
| } | |||||
| #controls .message { | |||||
| display: block; | |||||
| margin: 1px 0 0 0; | |||||
| padding: 0.25em 0.5em; | |||||
| text-align: center; | |||||
| font-family: sans-serif; | |||||
| font-size: 0.75em; | |||||
| color: #b00; | |||||
| } | |||||
| #controls .message:not(:empty) { | |||||
| background-color: #fff; | |||||
| } | |||||
| @media only screen and (max-width: 520px) { | |||||
| #controls .message { | |||||
| margin: 0 0 1px 1px; | |||||
| flex: 1 1 100%; | |||||
| padding: 0.5em 0.75em; | |||||
| font-size: 0.875em; | |||||
| } | |||||
| #controls .message:not(:empty) { | |||||
| outline: 1px solid #fff; | |||||
| border: 1px solid #e00; | |||||
| } | |||||
| } | |||||
| #scratchpad { | |||||
| opacity: 0; | |||||
| z-index: -1; | |||||
| pointer-events: none; | |||||
| } |
| <?php | |||||
| $filename = $_GET['f']; | |||||
| $stylesheet = "textfiles.css"; | |||||
| $stylesheet_href = $stylesheet . "?v=" . filemtime($stylesheet); | |||||
| ?> | |||||
| <html> | |||||
| <head> | |||||
| <meta name="viewport" content="width=device-width, initial-scale=1"> | |||||
| <link href='https://your.web.site/FontAwesome.css' type='text/css' rel='stylesheet' /> | |||||
| <link href='<?php echo $stylesheet_href; ?>' type='text/css' rel='stylesheet' /> | |||||
| <script type='text/javascript' src='textfiles.js'></script> | |||||
| </head> | |||||
| <body> | |||||
| <div class='compensator'> | |||||
| </div> | |||||
| <pre> | |||||
| <?php echo htmlentities(file_get_contents($filename)); ?> | |||||
| </pre> | |||||
| <div id='ui-elements-container'> | |||||
| <div id='controls'> | |||||
| <div class='buttons'> | |||||
| <button | |||||
| type='button' | |||||
| class='button copy-raw-link' | |||||
| title='Copy raw link to clipboard [k]' | |||||
| data-label='Raw link' | |||||
| data-main-action='copyRawLinkButtonClicked' | |||||
| data-success-message='Raw file URL copied to clipboard.' | |||||
| accesskey='k' | |||||
| ></button> | |||||
| <a | |||||
| class='button raw' | |||||
| title='View raw file [r]' | |||||
| data-label='View raw' | |||||
| accesskey='r' | |||||
| href='<?php echo str_replace('.txt', '', $filename); ?>/raw' | |||||
| ></a> | |||||
| <button | |||||
| type='button' | |||||
| class='button copy-text' | |||||
| title='Copy text to clipboard [c]' | |||||
| data-label='Copy text' | |||||
| data-main-action='copyTextButtonClicked' | |||||
| data-success-message='Text copied to clipboard.' | |||||
| accesskey='c' | |||||
| ></button> | |||||
| <button | |||||
| type='button' | |||||
| class='button copy-link' | |||||
| title='Copy link to clipboard [l]' | |||||
| data-label='Copy link' | |||||
| data-main-action='copyLinkButtonClicked' | |||||
| data-success-message='URL copied to clipboard.' | |||||
| accesskey='l' | |||||
| ></button> | |||||
| </div> | |||||
| <span class='message'></span> | |||||
| </div> | |||||
| <textarea id='scratchpad'></textarea> | |||||
| </div> | |||||
| </body> | |||||
| <script type='text/javascript'> | |||||
| copyLinkButtonClicked = () => { | |||||
| copyTextToClipboard(location); | |||||
| }; | |||||
| copyRawLinkButtonClicked = () => { | |||||
| copyTextToClipboard(location + "/raw"); | |||||
| }; | |||||
| copyTextButtonClicked = () => { | |||||
| selectElementContents(document.querySelector("pre")); | |||||
| document.execCommand("copy"); | |||||
| }; | |||||
| document.querySelectorAll("#controls button").forEach(button => { | |||||
| button.addActivateEvent((event) => { | |||||
| event.target.blur(); | |||||
| window[event.target.dataset["mainAction"]](); | |||||
| setMessage(event.target.dataset["successMessage"]); | |||||
| }); | |||||
| }); | |||||
| setTimeout(() => { | |||||
| selectElementContents(document.querySelector("pre")); | |||||
| }, 0); | |||||
| </script> | |||||
| </html> |
| /*******************************/ | |||||
| /* EVENT LISTENER MANIPULATION */ | |||||
| /*******************************/ | |||||
| /* Adds an event listener to a button (or other clickable element), attaching | |||||
| it to both "click" and "keyup" events (for use with keyboard navigation). | |||||
| Optionally also attaches the listener to the 'mousedown' event, making the | |||||
| element activate on mouse down instead of mouse up. */ | |||||
| Element.prototype.addActivateEvent = function(func, includeMouseDown) { | |||||
| let ael = this.activateEventListener = (event) => { if (event.button === 0 || event.key === ' ') func(event) }; | |||||
| if (includeMouseDown) this.addEventListener("mousedown", ael); | |||||
| this.addEventListener("click", ael); | |||||
| this.addEventListener("keyup", ael); | |||||
| } | |||||
| /* Removes event listener from a clickable element, automatically detaching it | |||||
| from all relevant event types. */ | |||||
| Element.prototype.removeActivateEvent = function() { | |||||
| let ael = this.activateEventListener; | |||||
| this.removeEventListener("mousedown", ael); | |||||
| this.removeEventListener("click", ael); | |||||
| this.removeEventListener("keyup", ael); | |||||
| } | |||||
| /***********/ | |||||
| /* HELPERS */ | |||||
| /***********/ | |||||
| function selectElementContents(element) { | |||||
| var range = document.createRange(); | |||||
| range.selectNodeContents(element); | |||||
| var selection = window.getSelection(); | |||||
| selection.removeAllRanges(); | |||||
| selection.addRange(range); | |||||
| } | |||||
| function copyTextToClipboard(string) { | |||||
| let scratchpad = document.querySelector("#scratchpad"); | |||||
| scratchpad.value = string; | |||||
| scratchpad.select(); | |||||
| document.execCommand("copy"); | |||||
| } | |||||
| function setMessage(string) { | |||||
| document.querySelector("#controls .message").innerText = string; | |||||
| } |
| <?php | |||||
| $base_url = "https://your.web.site/textfiles/"; | |||||
| if (isset($_GET['f'])) { | |||||
| render_file(); | |||||
| } else if ( isset($_POST['f']) | |||||
| && isset($_POST['p']) | |||||
| && $_POST['p'] == 'YOUR_API_KEY_GOES_HERE') { | |||||
| new_file(); | |||||
| } else { | |||||
| header ('Content-type: text/plain; charset=utf-8'); | |||||
| http_response_code(404); | |||||
| die("NO FILE FOR YOU!! COME BACK, ONE YEAR!"); | |||||
| } | |||||
| ## Functions | |||||
| function render_file() { | |||||
| if ($_GET['m'] == 'raw') { | |||||
| header ('Content-type: text/plain; charset=utf-8'); | |||||
| echo file_get_contents($_GET['f']); | |||||
| die; | |||||
| } else { | |||||
| header ('Content-type: text/html; charset=utf-8'); | |||||
| include_once(__DIR__ . "/textfiles.html"); | |||||
| die; | |||||
| } | |||||
| } | |||||
| function new_file() { | |||||
| `export LANG="en_US.UTF-8"`; | |||||
| $tmp_file_path = trim(`mktemp -q ./XXXXXXXX`); | |||||
| $tmp_file_name = `basename {$tmp_file_path}`; | |||||
| `rm {$tmp_file_path}`; | |||||
| file_put_contents("{$tmp_file_path}.txt", $_POST['f']); | |||||
| global $base_url; | |||||
| echo $base_url . $tmp_file_name; | |||||
| } | |||||
| ?> |