{param}" 46 | 47 | [admin] 48 | force_login = true 49 | nick = demo 50 | pass = demo 51 | 52 | [friends] 53 | ;friends[user] = pass 54 | ;friends[user] = pass 55 | 56 | [directories] 57 | images_path = data/i/ 58 | thumbnails_path = data/t/ 59 | logs_path = data/logs/ 60 | 61 | [proxy] 62 | ;proxy = hostname:port 63 | ;proxyauth = username:password 64 | ;proxytype = CURLPROXY_HTTP ; default, if not set 65 | ;proxytype = CURLPROXY_SOCKS4 66 | ;proxytype = CURLPROXY_SOCKS5 67 | 68 | ;URL_PREFIX type: 69 | ;proxy = http://your.page.com/proxy.cgi? 70 | ;proxyauth = username:password 71 | ;proxytype = URL_PREFIX 72 | 73 | [system] 74 | ;timezone = Europe/Vienna 75 | version = 1.42 76 | debug = false 77 | logs = false 78 | -------------------------------------------------------------------------------- /app/jbbcode/documentelement.class.php: -------------------------------------------------------------------------------- 1 | setTagName("Document"); 21 | $this->setNodeId(0); 22 | } 23 | 24 | /** 25 | * (non-PHPdoc) 26 | * @see JBBCode.ElementNode::getAsBBCode() 27 | * 28 | * Returns the BBCode representation of this document 29 | * 30 | * @return this document's bbcode representation 31 | */ 32 | public function getAsBBCode() 33 | { 34 | $s = ""; 35 | foreach($this->getChildren() as $child){ 36 | $s .= $child->getAsBBCode(); 37 | } 38 | 39 | return $s; 40 | } 41 | 42 | /** 43 | * (non-PHPdoc) 44 | * @see JBBCode.ElementNode::getAsHTML() 45 | * 46 | * Documents don't add any html. They only exist as a container for their 47 | * children, so getAsHTML() simply iterates through the document's children, 48 | * returning their html. 49 | * 50 | * @return the HTML representation of this document 51 | */ 52 | public function getAsHTML() 53 | { 54 | $s = ""; 55 | foreach($this->getChildren() as $child) 56 | $s .= $child->getAsHTML(); 57 | 58 | return $s; 59 | } 60 | 61 | public function accept(NodeVisitor $visitor) 62 | { 63 | $visitor->visitDocumentElement($this); 64 | } 65 | 66 | } 67 | -------------------------------------------------------------------------------- /app/user.class.php: -------------------------------------------------------------------------------- 1 | true, "is_visitor" => false]; 36 | } 37 | 38 | // Legacy: Visitors and Friends. 39 | $visitors = array_merge( 40 | Config::get_safe("friends", []), 41 | Config::get_safe("visitor", []) 42 | ); 43 | if(!empty($visitors) && isset($visitors[$nick]) && $visitors[$nick] === $pass){ 44 | $_SESSION[User::SESSION_NAME] = 'visitor'; 45 | return ["logged_in" => false, "is_visitor" => true]; 46 | } 47 | 48 | Log::put("login_fails", $nick); 49 | throw new Exception(__("The nick or password is incorrect.")); 50 | } 51 | 52 | public static function logout(){ 53 | if(!Config::get_safe("force_login", false)){ 54 | throw new Exception(__("You can't log out. There is no account.")); 55 | } 56 | 57 | if(!self::is_logged_in() && !self::is_visitor()){ 58 | throw new Exception(__("You are not even logged in.")); 59 | } 60 | 61 | $_SESSION[User::SESSION_NAME] = false; 62 | return true; 63 | } 64 | } -------------------------------------------------------------------------------- /app/lang/ru.ini: -------------------------------------------------------------------------------- 1 | ; index.html 2 | Show More = "Показать больше" 3 | 4 | Login = "Войти" 5 | Logout = "Выйти" 6 | 7 | Nick = "Логин" 8 | Password = "Пароль" 9 | Cancel = "Отмена" 10 | 11 | Post = "Пост" 12 | 13 | Edit Post = "Редактировать пост" 14 | Change Date = "Изменить дату" 15 | Hide from Timeline = "Скрыть из ленты" 16 | Show on Timeline = "Показать в ленте" 17 | Delete Post = "Удалить пост" 18 | 19 | Drag photos here = "Перетащите сюда фото" 20 | Drop photos here = "Оставь фото здесь" 21 | What's on your mind? = "О чем ты думаешь?" 22 | Feeling = "Ощущения" 23 | How are you feeling? = "Как ты себя чувствуешь?" 24 | With = "В компании" 25 | Who are you with? = "С кем ты?" 26 | At = "Локация" 27 | Where are you? = "Где ты?" 28 | Save = "Сохранить" 29 | 30 | January = "Январь" 31 | February = "Февраль" 32 | March = "Март" 33 | April = "Апрель" 34 | May = "Май" 35 | June = "Июнь" 36 | July = "Июль" 37 | August = "Август" 38 | September = "Сентябрь" 39 | October = "Октябрь" 40 | November = "Ноябрь" 41 | December = "Декабрь" 42 | 43 | Time: = "Время:" 44 | Hour: = "Чаc:" 45 | Minute: = "Минута:" 46 | 47 | This post will be deleted and you'll no longer be able to find it. You can also edit this post if you just want to change something. = "Этот пост будет удален, и вы больше не сможете его найти. Вы также можете отредактировать этот пост, если просто хотите что-то изменить" 48 | 49 | with = "с" 50 | here: = "здесь:" 51 | 52 | Public = "Публичный" 53 | Friends = "Друзья" 54 | Only me = "Только я" 55 | 56 | Show hidden content = "Показать скрытый контент" 57 | Show all posts = "Показать все посты" 58 | 59 | ; user.class.php 60 | You are already logged in. = "Вы уже залогинены." 61 | The nick or password is incorrect. = "Логин или пароль неверный." 62 | You can't log out. There is no account. = "Вы не можете выйти. Вы не залогинены." 63 | You are not even logged in. = "Вы не авторизовались." 64 | 65 | ; post.class.php 66 | You need to be logged in to perform this action. = "Вы должны быть залогинены, чтобы выполнить это действие." 67 | No data. = "Нет данных." 68 | -------------------------------------------------------------------------------- /app/lang/bs.ini: -------------------------------------------------------------------------------- 1 | ; index.html 2 | Show More = "Prikaži još" 3 | 4 | Login = "Prijava" 5 | Logout = "Odjava" 6 | 7 | Nick = "Nadimak" 8 | Password = "Šifra" 9 | Cancel = "Zatvori" 10 | 11 | Post = "Objavi" 12 | 13 | Edit Post = "Izmijeni Objavu" 14 | Change Date = "Promijeni Datum" 15 | Hide from Timeline = "Sakrij sa Vremenske Linije" 16 | Show on Timeline = "Prikaži na Vremenskoj Liniji" 17 | Delete Post = "Izbriši objavu" 18 | 19 | Drag photos here = "Privuci fotografije ovdje" 20 | Drop photos here = "Privuci fotografije ovdje" 21 | What's on your mind? = "Šta vam je na umu?" 22 | Feeling = "Osjećaj" 23 | How are you feeling? = "Kako se osjećate?" 24 | With = "Sa" 25 | Who are you with? = "S kim ste?" 26 | At = "Gdje" 27 | Where are you? = "Gdje se nalazite?" 28 | Save = "Sačuvaj" 29 | 30 | January = "Januar" 31 | February = "Februar" 32 | March = "Mart" 33 | April = "April" 34 | May = "Maj" 35 | June = "Juni" 36 | July = "Juli" 37 | August = "Avgust" 38 | September = "Septembar" 39 | October = "Oktobar" 40 | November = "Novembar" 41 | December = "Decembar" 42 | 43 | Time: = "Vrijeme:" 44 | Hour: = "Sati:" 45 | Minute: = "Minute:" 46 | 47 | This post will be deleted and you'll no longer be able to find it. You can also edit this post if you just want to change something. = "Ova će objava biti izbrisana i nećete je više moći pronaći. Takođe možete izmijeniti ovu objavu ako želite izvršiti izmjene." 48 | 49 | with = "sa" 50 | here: = "ovdje:" 51 | 52 | Public = "Javno" 53 | Friends = "Prijatelji" 54 | Only me = "Samo ja" 55 | 56 | Show hidden content = "Prikaži skriveni sadržaj" 57 | Show all posts = "Prikaži sve objave" 58 | 59 | ; user.class.php 60 | You are already logged in. = "Već ste prijavljeni." 61 | The nick or password is incorrect. = "Nadimak ili šifra su pogrešni." 62 | You can't log out. There is no account. = "Ne možete se odjaviti. Račun ne postoji." 63 | You are not even logged in. = "Niste čak ni prijavljeni." 64 | 65 | ; post.class.php 66 | You need to be logged in to perform this action. = "Morate biti priavljeni da biste izvršili ovu radnju." 67 | No data. = "Nema podataka." 68 | -------------------------------------------------------------------------------- /app/lang/nl.ini: -------------------------------------------------------------------------------- 1 | ; index.html 2 | Show More = "Verder Lezen" 3 | 4 | Login = "Aanmelden" 5 | Logout = "Afmelden" 6 | 7 | Nick = "Gebruiker" 8 | Password = "Wachtwoord" 9 | Cancel = "Annuleren" 10 | 11 | Post = "Bericht" 12 | 13 | Edit Post = "Wijzig Bericht" 14 | Change Date = "Wijzig Datum" 15 | Hide from Timeline = "Verbergen van Tijdlijn" 16 | Show on Timeline = "Laten zien op Tijdlijn" 17 | Delete Post = "Verwijder Bericht" 18 | 19 | Drag photos here = "Sleep foto's hier" 20 | Drop photos here = "Gooi foto's hier" 21 | What's on your mind? = "Waar ben je mee bezig?" 22 | Feeling = "Gevoel" 23 | How are you feeling? = "Hoe voel je je?" 24 | With = "Met" 25 | Who are you with? = "Met wie ben je?" 26 | At = "Bij" 27 | Where are you? = "Waar ben je?" 28 | Save = "Opslaan" 29 | 30 | January = "Januari" 31 | February = "Februari" 32 | March = "Maart" 33 | April = "April" 34 | May = "Mei" 35 | June = "Juni" 36 | July = "Juli" 37 | August = "Augustus" 38 | September = "September" 39 | October = "Oktober" 40 | November = "November" 41 | December = "December" 42 | 43 | Time: = "Tijd:" 44 | Hour: = "Uur:" 45 | Minute: = "Minuten:" 46 | 47 | This post will be deleted and you'll no longer be able to find it. You can also edit this post if you just want to change something. = "Dit bericht word verwijderd en kan niet meer worden gevonden. Je kan ook het bericht wijzigen als je dat liever wilt." 48 | 49 | with = "met" 50 | here: = "Bij:" 51 | 52 | Public = "Openbaar" 53 | Friends = "Vrienden" 54 | Only me = "Alleen ik" 55 | 56 | Show hidden content = "Laat verborgen berichten zien" 57 | Show all posts = "Alle berichten zien" 58 | 59 | ; user.class.php 60 | You are already logged in. = "Je bent al ingelogt." 61 | The nick or password is incorrect. = "De gebruiken of het Wachtwoord is onjuist." 62 | You can't log out. There is no account. = "Je kan niet Afmelden. Er is geen Account." 63 | You are not even logged in. = "Je ben niet eens ingelogt." 64 | 65 | ; post.class.php 66 | You need to be logged in to perform this action. = "Je moet ingelogged zijn om deze actie uit te voeren." 67 | No data. = "Geen data." 68 | -------------------------------------------------------------------------------- /app/lang/en.ini: -------------------------------------------------------------------------------- 1 | ; index.html 2 | Show More = "Show More" 3 | 4 | Login = "Login" 5 | Logout = "Logout" 6 | 7 | Nick = "Nick" 8 | Password = "Password" 9 | Cancel = "Cancel" 10 | 11 | Post = "Post" 12 | 13 | Edit Post = "Edit Post" 14 | Change Date = "Change Date" 15 | Hide from Timeline = "Hide from Timeline" 16 | Show on Timeline = "Show on Timeline" 17 | Delete Post = "Delete Post" 18 | 19 | Drag photos here = "Drag photos here" 20 | Drop photos here = "Drop photos here" 21 | What's on your mind? = "What's on your mind?" 22 | Feeling = "Feeling" 23 | How are you feeling? = "How are you feeling?" 24 | With = "With" 25 | Who are you with? = "Who are you with?" 26 | At = "At" 27 | Where are you? = "Where are you?" 28 | Save = "Save" 29 | 30 | January = "January" 31 | February = "February" 32 | March = "March" 33 | April = "April" 34 | May = "May" 35 | June = "June" 36 | July = "July" 37 | August = "August" 38 | September = "September" 39 | October = "October" 40 | November = "November" 41 | December = "December" 42 | 43 | Time: = "Time:" 44 | Hour: = "Hour:" 45 | Minute: = "Minute:" 46 | 47 | This post will be deleted and you'll no longer be able to find it. You can also edit this post if you just want to change something. = "This post will be deleted and you'll no longer be able to find it. You can also edit this post if you just want to change something." 48 | 49 | with = "with" 50 | here: = "here:" 51 | 52 | Public = "Public" 53 | Friends = "Friends" 54 | Only me = "Only me" 55 | 56 | Show hidden content = "Show hidden content" 57 | Show all posts = "Show all posts" 58 | 59 | ; user.class.php 60 | You are already logged in. = "You are already logged in." 61 | The nick or password is incorrect. = "The nick or password is incorrect." 62 | You can't log out. There is no account. = "You can't log out. There is no account." 63 | You are not even logged in. = "You are not even logged in." 64 | 65 | ; post.class.php 66 | You need to be logged in to perform this action. = "You need to be logged in to perform this action." 67 | No data. = "No data." 68 | -------------------------------------------------------------------------------- /app/lang/cz.ini: -------------------------------------------------------------------------------- 1 | ; index.html 2 | Show More = "Zobrazit více" 3 | 4 | Login = "Přihlásit se" 5 | Logout = "Odhlásit se" 6 | 7 | Nick = "Přihlašovací jméno" 8 | Password = "Heslo" 9 | Cancel = "Zrušit" 10 | 11 | Post = "Nový příspěvek" 12 | 13 | Edit Post = "Upravit příspěvek" 14 | Change Date = "Změnit datum" 15 | Hide from Timeline = "Skrýt na časové ose" 16 | Show on Timeline = "Zobrazit na časové ose" 17 | Delete Post = "Odstranit příspěvek" 18 | 19 | Drag photos here = "Sem přesuňte fotografie" 20 | Drop photos here = "Zde vložte fotografie" 21 | What's on your mind? = "Na co myslíš?" 22 | Feeling = "Pocit" 23 | How are you feeling? = "Jak se cítíš?" 24 | With = "Spolu s" 25 | Who are you with? = "S kým jsi?" 26 | At = "Místo" 27 | Where are you? = "Kde jsi?" 28 | Save = "Uložit" 29 | 30 | January = "Leden" 31 | February = "Únor" 32 | March = "Březen" 33 | April = "Duben" 34 | May = "Květen" 35 | June = "Červen" 36 | July = "Červenec" 37 | August = "Srpen" 38 | September = "Září" 39 | October = "Říjen" 40 | November = "Listopad" 41 | December = "Prosinec" 42 | 43 | Time: = "Čas:" 44 | Hour: = "Hodina:" 45 | Minute: = "Minuta:" 46 | 47 | This post will be deleted and you'll no longer be able to find it. You can also edit this post if you just want to change something. = "Tento příspěvek bude odstraněn a už ho nebude možné najít. Pokud chcete něco změnit, můžete tento příspěvek také upravit." 48 | 49 | with = "s" 50 | here: = "zde:" 51 | 52 | Public = "Veřejné" 53 | Friends = "Přátelé" 54 | Only me = "Jen já" 55 | 56 | Show hidden content = "Zobrazit skrytý obsah" 57 | Show all posts = "Zobrazit všechny příspěvky" 58 | 59 | ; user.class.php 60 | You are already logged in. = "Už jsi přihlášený." 61 | The nick or password is incorrect. = "Přihlašovací jméno nebo heslo je nesprávné." 62 | You can't log out. There is no account. = "Nemůžeš se odhlásit. Neexistuje žádný účet." 63 | You are not even logged in. = "Nejsi ani přihlášený." 64 | 65 | ; post.class.php 66 | You need to be logged in to perform this action. = "Na provedení této akce musíš být přihlášený." 67 | No data. = "Žádná data." 68 | -------------------------------------------------------------------------------- /app/lang/es.ini: -------------------------------------------------------------------------------- 1 | ; index.html 2 | Show More = "Mostrar más" 3 | 4 | Login = "Iniciar sesión" 5 | Logout = "Cerrar sesión" 6 | 7 | Nick = "Apodo" 8 | Password = "Contraseña" 9 | Cancel = "Cancelar" 10 | 11 | Post = "Publicar" 12 | 13 | Edit Post = "Editar publicación" 14 | Change Date = "Cambiar fecha" 15 | Hide from Timeline = "Ocultar de la línea de tiempo" 16 | Show on Timeline = "Mostrar en la línea de tiempo" 17 | Delete Post = "Eliminar publicación" 18 | 19 | Drag photos here = "Arrastra aquí las fotos" 20 | Drop photos here = "Suelta aquí las fotos" 21 | What's on your mind? = "¿Qué estas pensando?" 22 | Feeling = "Sintiendo" 23 | How are you feeling? = "¿Como te sientes?" 24 | With = "Con" 25 | Who are you with? = "¿Con quién estás?" 26 | At = "En" 27 | Where are you? = "¿Dónde estás?" 28 | Save = "Guardar" 29 | 30 | January = "Enero" 31 | February = "Febrero" 32 | March = "Marzo" 33 | April = "Abril" 34 | May = "Mayo" 35 | June = "Junio" 36 | July = "Julio" 37 | August = "Agosto" 38 | September = "Septiembre" 39 | October = "Octubre" 40 | November = "Noviembre" 41 | December = "Diciembre" 42 | 43 | Time: = "Hora:" 44 | Hour: = "Hora:" 45 | Minute: = "Minuto:" 46 | 47 | This post will be deleted and you'll no longer be able to find it. You can also edit this post if you just want to change something. = "Esta publicación se eliminará y ya no podrá ser encontrada. También puedes editar esta publicación si solo deseas cambiar algo." 48 | 49 | with = "con" 50 | here: = "aqui:" 51 | 52 | Public = "Público" 53 | Friends = "Amigos" 54 | Only me = "Solo yo" 55 | 56 | Show hidden content = "Mostrar contenido oculto" 57 | Show all posts = "Mostrar todas las publicaciones" 58 | 59 | ; user.class.php 60 | You are already logged in. = "Ya has iniciado sesión." 61 | The nick or password is incorrect. = "El apodo o la contraseña son incorrectos." 62 | You can't log out. There is no account. = "No puede cerrar sesión. No hay cuenta." 63 | You are not even logged in. = "Ni siquiera estás conectado." 64 | 65 | ; post.class.php 66 | You need to be logged in to perform this action. = "Debe haber iniciado una sesión para realizar esta acción." 67 | No data. = "Sin datos." 68 | -------------------------------------------------------------------------------- /app/lang/sk.ini: -------------------------------------------------------------------------------- 1 | ; index.html 2 | Show More = "Zobraziť viac" 3 | 4 | Login = "Prihlásiť sa" 5 | Logout = "Odhlásiť sa" 6 | 7 | Nick = "Prihlasovacie meno" 8 | Password = "Heslo" 9 | Cancel = "Zrušiť" 10 | 11 | Post = "Nový príspevok" 12 | 13 | Edit Post = "Upraviť príspevok" 14 | Change Date = "Zmeniť dátum" 15 | Hide from Timeline = "Skryť na časovej osi" 16 | Show on Timeline = "Zobraziť na časovej osi" 17 | Delete Post = "Odstrániť príspevok" 18 | 19 | Drag photos here = "Sem presuňte fotografie" 20 | Drop photos here = "Tu vložte fotografie" 21 | What's on your mind? = "Na čo myslíš?" 22 | Feeling = "Pocit" 23 | How are you feeling? = "Ako sa cítiš?" 24 | With = "Spolu s" 25 | Who are you with? = "S kým si?" 26 | At = "Miesto" 27 | Where are you? = "Kde si?" 28 | Save = "Uložiť" 29 | 30 | January = "Január" 31 | February = "Február" 32 | March = "Marec" 33 | April = "Apríl" 34 | May = "Máj" 35 | June = "Jún" 36 | July = "Júl" 37 | August = "August" 38 | September = "September" 39 | October = "Október" 40 | November = "November" 41 | December = "December" 42 | 43 | Time: = "Čas:" 44 | Hour: = "Hodina:" 45 | Minute: = "Minúta:" 46 | 47 | This post will be deleted and you'll no longer be able to find it. You can also edit this post if you just want to change something. = "Tento príspevok bude odstránený a už ho nebudete môcť nájsť. Tento príspevok môžete tiež upraviť, ak chcete niečo zmeniť." 48 | 49 | with = "s" 50 | here: = "tu:" 51 | 52 | Public = "Verejné" 53 | Friends = "Priatelia" 54 | Only me = "Iba ja" 55 | 56 | Show hidden content = "Zobraziť skrytý obsah" 57 | Show all posts = "Zobraziť všetky príspevky" 58 | 59 | ; user.class.php 60 | You are already logged in. = "Už si prihlásený." 61 | The nick or password is incorrect. = "Prihlasovacie meno alebo heslo je nesprávne." 62 | You can't log out. There is no account. = "Nemôžeš sa odhlásiť. Neexistuje žiadny účet." 63 | You are not even logged in. = "Nie si ani prihlásený." 64 | 65 | ; post.class.php 66 | You need to be logged in to perform this action. = "Na vykonanie tejto akcie musíš byť prihlásený." 67 | No data. = "Žiadne dáta." 68 | -------------------------------------------------------------------------------- /app/lang/fr.ini: -------------------------------------------------------------------------------- 1 | ; index.html 2 | Show More = "Voir plus" 3 | 4 | Login = "Connexion" 5 | Logout = "Déconnexion" 6 | 7 | Nick = "Pseudo" 8 | Password = "Mot de passe" 9 | Cancel = "Annuler" 10 | 11 | Post = "Publier" 12 | 13 | Edit Post = "Modifier la publication" 14 | Change Date = "Changer la date" 15 | Hide from Timeline = "Cacher du mur" 16 | Show on Timeline = "Montrer sur le mur" 17 | Delete Post = "Supprimer la publication" 18 | 19 | Drag photos here = "Glisser les photos ici" 20 | Drop photos here = "Déposer les photos ici" 21 | What's on your mind? = "Que souhaitez-vous partager ?" 22 | Feeling = "Je me sens" 23 | How are you feeling? = "Comment vous sentez-vous ?" 24 | With = "Avec" 25 | Who are you with? = "Avec qui êtes-vous ?" 26 | At = "À" 27 | Where are you? = "Où êtes-vous ?" 28 | Save = "Enregistrer" 29 | 30 | January = "Janvier" 31 | February = "Février" 32 | March = "Mars" 33 | April = "Avril" 34 | May = "Mai" 35 | June = "Juin" 36 | July = "Juillet" 37 | August = "Août" 38 | September = "Septembre" 39 | October = "Octobre" 40 | November = "Novembre" 41 | December = "Décembre" 42 | 43 | Time: = "Heure :" 44 | Hour: = "Heure :" 45 | Minute: = "Minutes :" 46 | 47 | This post will be deleted and you'll no longer be able to find it. You can also edit this post if you just want to change something. = "Cette publication sera supprimée et vous ne pourrez plus la retrouver. Vous pouvez aussi la modifier si vous souhaitez simplement changer quelque chose." 48 | 49 | with = "avec" 50 | here: = "à" 51 | 52 | Public = "Publique" 53 | Friends = "Amis" 54 | Only me = "Seulement moi" 55 | 56 | Show hidden content = "Montrer le contenu caché" 57 | Show all posts = "Montrer toutes les publications" 58 | 59 | ; user.class.php 60 | You are already logged in. = "Vous êtes déjà connecté." 61 | The nick or password is incorrect. = "Votre pseudo ou mot de passe est incorrect." 62 | You can't log out. There is no account. = "Vous ne pouvez pas vous déconnecter, les comptes sont désactivés." 63 | You are not even logged in. = "Vous n’êtes pas connecté." 64 | 65 | ; post.class.php 66 | You need to be logged in to perform this action. = "Vous devez être connecté pour pouvoir faire ça." 67 | No data. = "Aucune données." 68 | -------------------------------------------------------------------------------- /app/jbbcode/visitors/nestlimitvisitor.class.php: -------------------------------------------------------------------------------- 1 | getChildren() as $child) { 23 | $child->accept($this); 24 | } 25 | } 26 | 27 | public function visitTextNode(\JBBCode\TextNode $textNode) 28 | { 29 | /* Nothing to do. Text nodes don't have tag names or children. */ 30 | } 31 | 32 | public function visitElementNode(\JBBCode\ElementNode $elementNode) 33 | { 34 | $tagName = strtolower($elementNode->getTagName()); 35 | 36 | /* Update the current depth for this tag name. */ 37 | if (isset($this->depth[$tagName])) { 38 | $this->depth[$tagName]++; 39 | } else { 40 | $this->depth[$tagName] = 1; 41 | } 42 | 43 | /* Check if $elementNode is nested too deeply. */ 44 | if ($elementNode->getCodeDefinition()->getNestLimit() != -1 && 45 | $elementNode->getCodeDefinition()->getNestLimit() < $this->depth[$tagName]) { 46 | /* This element is nested too deeply. We need to remove it and not visit any 47 | * of its children. */ 48 | $elementNode->getParent()->removeChild($elementNode); 49 | } else { 50 | /* This element is not nested too deeply. Visit all of its children. */ 51 | foreach ($elementNode->getChildren() as $child) { 52 | $child->accept($this); 53 | } 54 | } 55 | 56 | /* Now that we're done visiting this node, decrement the depth. */ 57 | $this->depth[$tagName]--; 58 | } 59 | 60 | } 61 | -------------------------------------------------------------------------------- /app/lang/de.ini: -------------------------------------------------------------------------------- 1 | ; index.html 2 | Show More = "Mehr anzeigen" 3 | 4 | Login = "Anmelden" 5 | Logout = "Abmelden" 6 | 7 | Nick = "Benutzername" 8 | Password = "Kennwort" 9 | Cancel = "Abbrechen" 10 | 11 | Post = "Verfasse einen Beitrag" 12 | 13 | Edit Post = "Beitrag bearbeiten" 14 | Change Date = "Datum ändern" 15 | Hide from Timeline = "In der Chronik verbergen" 16 | Show on Timeline = "In der Chronik anzeigen" 17 | Delete Post = "Beitrag löschen" 18 | 19 | Drag photos here = "Fotos hierher ziehen" 20 | Drop photos here = "Fotos hierher ablegen" 21 | What's on your mind? = "Was machst du gerade?" 22 | Feeling = "Fühlen" 23 | How are you feeling? = "Wie fühlst du dich?" 24 | With = "Mit" 25 | Who are you with? = "Wer begleitet dich?" 26 | At = "Hier" 27 | Where are you? = "Wo bist du?" 28 | Save = "Speichern" 29 | 30 | January = "Januar" 31 | February = "Februar" 32 | March = "März" 33 | April = "April" 34 | May = "Mai" 35 | June = "Juni" 36 | July = "Juli" 37 | August = "August" 38 | September = "September" 39 | October = "Oktober" 40 | November = "November" 41 | December = "Dezember" 42 | 43 | Time: = "Zeit:" 44 | Hour: = "Stunde:" 45 | Minute: = "Minute:" 46 | 47 | This post will be deleted and you'll no longer be able to find it. You can also edit this post if you just want to change something. = "Dieser Beitrag wird gelöscht und du wirst ihn nicht mehr finden können. Du kannst den Beitrag auch bearbeiten, wenn du nur etwas ändern möchtest." 48 | 49 | with = "mit" 50 | here: = "hier:" 51 | 52 | Public = "Öffentlich" 53 | Friends = "Freunde" 54 | Only me = "Nur ich" 55 | 56 | Show hidden content = "Verborgenen Inhalt anzeigen" 57 | Show all posts = "Alle Beiträge anzeigen" 58 | 59 | ; user.class.php 60 | You are already logged in. = "Du bist bereits eingeloggt." 61 | The nick or password is incorrect. = "Der Benutzername oder das Kennwort ist falsch." 62 | You can't log out. There is no account. = "Du kannst dich nicht abmelden. Es gibt kein Konto." 63 | You are not even logged in. = "Du bist nicht mal eingeloggt." 64 | 65 | ; post.class.php 66 | You need to be logged in to perform this action. = "Du musst angemeldet sein, um diese Aktion durchzuführen." 67 | No data. = "Keine Daten." 68 | -------------------------------------------------------------------------------- /app/jbbcode/textnode.class.php: -------------------------------------------------------------------------------- 1 | value = $val; 24 | } 25 | 26 | public function accept(NodeVisitor $visitor) 27 | { 28 | $visitor->visitTextNode($this); 29 | } 30 | 31 | /** 32 | * (non-PHPdoc) 33 | * @see JBBCode.Node::isTextNode() 34 | * 35 | * returns true 36 | */ 37 | public function isTextNode() 38 | { 39 | return true; 40 | } 41 | 42 | /** 43 | * Returns the text string value of this text node. 44 | * 45 | * @return string 46 | */ 47 | public function getValue() 48 | { 49 | return $this->value; 50 | } 51 | 52 | /** 53 | * (non-PHPdoc) 54 | * @see JBBCode.Node::getAsText() 55 | * 56 | * Returns the text representation of this node. 57 | * 58 | * @return this node represented as text 59 | */ 60 | public function getAsText() 61 | { 62 | return $this->getValue(); 63 | } 64 | 65 | /** 66 | * (non-PHPdoc) 67 | * @see JBBCode.Node::getAsBBCode() 68 | * 69 | * Returns the bbcode representation of this node. (Just its value) 70 | * 71 | * @return this node represented as bbcode 72 | */ 73 | public function getAsBBCode() 74 | { 75 | return $this->getValue(); 76 | } 77 | 78 | /** 79 | * (non-PHPdoc) 80 | * @see JBBCode.Node::getAsHTML() 81 | * 82 | * Returns the html representation of this node. (Just its value) 83 | * 84 | * @return this node represented as HTML 85 | */ 86 | public function getAsHTML() 87 | { 88 | return $this->getValue(); 89 | } 90 | 91 | /** 92 | * Edits the text value contained within this text node. 93 | * 94 | * @param newValue the new text value of the text node 95 | */ 96 | public function setValue($newValue) 97 | { 98 | $this->value = $newValue; 99 | } 100 | 101 | } 102 | -------------------------------------------------------------------------------- /app/jbbcode/defaultcodedefinitionset.class.php: -------------------------------------------------------------------------------- 1 | {param}'); 24 | array_push($this->definitions, $builder->build()); 25 | 26 | /* [i] italics tag */ 27 | $builder = new CodeDefinitionBuilder('i', '{param}'); 28 | array_push($this->definitions, $builder->build()); 29 | 30 | /* [u] underline tag */ 31 | $builder = new CodeDefinitionBuilder('u', '{param}'); 32 | array_push($this->definitions, $builder->build()); 33 | 34 | $urlValidator = new \JBBCode\validators\UrlValidator(); 35 | 36 | /* [url] link tag */ 37 | $builder = new CodeDefinitionBuilder('url', '{param}'); 38 | $builder->setParseContent(false)->setBodyValidator($urlValidator); 39 | array_push($this->definitions, $builder->build()); 40 | 41 | /* [url=http://example.com] link tag */ 42 | $builder = new CodeDefinitionBuilder('url', '{param}'); 43 | $builder->setUseOption(true)->setParseContent(true)->setOptionValidator($urlValidator); 44 | array_push($this->definitions, $builder->build()); 45 | 46 | /* [img] image tag */ 47 | $builder = new CodeDefinitionBuilder('img', '
| ' + 55 | ' | ' + 56 | ' | '+this.months[this.m]+' '+this.y+' | ' + 57 | '' + 58 | ' | ' + 59 | ' | ||
|---|---|---|---|---|---|---|
| Mo | ' + 62 | 'Tu | ' + 63 | 'We | ' + 64 | 'Th | ' + 65 | 'Fr | ' + 66 | 'Sa | ' + 67 | 'Su | ' + 68 | '
| ");
156 | td.text(d);
157 |
158 | if(active) {
159 | td.addClass("active");
160 | }
161 |
162 | if(this.today[0] == d && this.today[1] == m && this.today[2] == y) {
163 | td.addClass("today");
164 | }
165 |
166 | var selected = this.selected;
167 | if(selected[0].val() == d && selected[1].val() == m + 1 && selected[2].val() == y) {
168 | td.addClass("selected");
169 | selected[3] = td;
170 | }
171 |
172 | $(td).click(function(){
173 | console.log("Set date: " + y + "/" + (m + 1) + "/" + d);
174 |
175 | selected[0].val(d);
176 | selected[1].val(m + 1);
177 | selected[2].val(y);
178 |
179 | $(selected[3]).removeClass("selected");
180 | selected[3] = this;
181 | $(selected[3]).addClass("selected");
182 | });
183 |
184 | $(this.tr).append(td);
185 | this.i++;
186 | },
187 |
188 | init: function (container) {
189 | var today = new Date();
190 | this.today = [today.getDate(), today.getMonth(), today.getFullYear()];
191 |
192 | this.selected = [
193 | $(container).find(".day"),
194 | $(container).find(".month"),
195 | $(container).find(".year")
196 | ];
197 |
198 | var months = $(container).find(".month_names").val();
199 | this.months = months.split(",");
200 |
201 | this.set_date(this.selected[1].val() - 1, this.selected[2].val());
202 |
203 | this.build_table(container);
204 | this.load_table();
205 | }
206 | };
207 |
208 | datepick.init(container);
209 | return datepick;
210 | };
--------------------------------------------------------------------------------
/app/jbbcode/elementnode.class.php:
--------------------------------------------------------------------------------
1 | children = array();
36 | $this->nestDepth = 0;
37 | }
38 |
39 | /**
40 | * Accepts the given NodeVisitor. This is part of an implementation
41 | * of the Visitor pattern.
42 | *
43 | * @param $nodeVisitor the visitor attempting to visit this node
44 | */
45 | public function accept(NodeVisitor $nodeVisitor)
46 | {
47 | $nodeVisitor->visitElementNode($this);
48 | }
49 |
50 | /**
51 | * Gets the CodeDefinition that defines this element.
52 | *
53 | * @return this element's code definition
54 | */
55 | public function getCodeDefinition()
56 | {
57 | return $this->codeDefinition;
58 | }
59 |
60 | /**
61 | * Sets the CodeDefinition that defines this element.
62 | *
63 | * @param codeDef the code definition that defines this element node
64 | */
65 | public function setCodeDefinition(CodeDefinition $codeDef)
66 | {
67 | $this->codeDefinition = $codeDef;
68 | $this->setTagName($codeDef->getTagName());
69 | }
70 |
71 | /**
72 | * Returns the tag name of this element.
73 | *
74 | * @return the element's tag name
75 | */
76 | public function getTagName()
77 | {
78 | return $this->tagName;
79 | }
80 |
81 | /**
82 | * Returns the attribute (used as the option in bbcode definitions) of this element.
83 | *
84 | * @return the attribute of this element
85 | */
86 | public function getAttribute()
87 | {
88 | return $this->attribute;
89 | }
90 |
91 | /**
92 | * Returns all the children of this element.
93 | *
94 | * @return an array of this node's child nodes
95 | */
96 | public function getChildren()
97 | {
98 | return $this->children;
99 | }
100 |
101 | /**
102 | * (non-PHPdoc)
103 | * @see JBBCode.Node::getAsText()
104 | *
105 | * Returns the element as text (not including any bbcode markup)
106 | *
107 | * @return the plain text representation of this node
108 | */
109 | public function getAsText()
110 | {
111 | if ($this->codeDefinition) {
112 | return $this->codeDefinition->asText($this);
113 | } else {
114 | $s = "";
115 | foreach ($this->getChildren() as $child)
116 | $s .= $child->getAsText();
117 | return $s;
118 | }
119 | }
120 |
121 | /**
122 | * (non-PHPdoc)
123 | * @see JBBCode.Node::getAsBBCode()
124 | *
125 | * Returns the element as bbcode (with all unclosed tags closed)
126 | *
127 | * @return the bbcode representation of this element
128 | */
129 | public function getAsBBCode()
130 | {
131 | $str = "[".$this->tagName;
132 | if (!empty($this->attribute)) {
133 |
134 | foreach($this->attribute as $key => $value){
135 | if($key == $this->tagName){
136 | $str .= "=".$value;
137 | }
138 | else{
139 | $str .= " ".$key."=" . $value;
140 | }
141 | }
142 | }
143 | $str .= "]";
144 | foreach ($this->getChildren() as $child) {
145 | $str .= $child->getAsBBCode();
146 | }
147 | $str .= "[/".$this->tagName."]";
148 |
149 | return $str;
150 | }
151 |
152 | /**
153 | * (non-PHPdoc)
154 | * @see JBBCode.Node::getAsHTML()
155 | *
156 | * Returns the element as html with all replacements made
157 | *
158 | * @return the html representation of this node
159 | */
160 | public function getAsHTML()
161 | {
162 | if($this->codeDefinition) {
163 | return $this->codeDefinition->asHtml($this);
164 | } else {
165 | return "";
166 | }
167 | }
168 |
169 | /**
170 | * Adds a child to this node's content. A child may be a TextNode, or
171 | * another ElementNode... or anything else that may extend the
172 | * abstract Node class.
173 | *
174 | * @param child the node to add as a child
175 | */
176 | public function addChild(Node $child)
177 | {
178 | array_push($this->children, $child);
179 | $child->setParent($this);
180 | }
181 |
182 | /**
183 | * Removes a child from this node's contnet.
184 | *
185 | * @param child the child node to remove
186 | */
187 | public function removeChild(Node $child)
188 | {
189 | foreach ($this->children as $key => $value) {
190 | if ($value == $child)
191 | unset($this->children[$key]);
192 | }
193 | }
194 |
195 | /**
196 | * Sets the tag name of this element node.
197 | *
198 | * @param tagName the element's new tag name
199 | */
200 | public function setTagName($tagName)
201 | {
202 | $this->tagName = $tagName;
203 | }
204 |
205 | /**
206 | * Sets the attribute (option) of this element node.
207 | *
208 | * @param attribute the attribute of this element node
209 | */
210 | public function setAttribute($attribute)
211 | {
212 | $this->attribute = $attribute;
213 | }
214 |
215 | /**
216 | * Traverses the parse tree upwards, going from parent to parent, until it finds a
217 | * parent who has the given tag name. Returns the parent with the matching tag name
218 | * if it exists, otherwise returns null.
219 | *
220 | * @param str the tag name to search for
221 | *
222 | * @return the closest parent with the given tag name
223 | */
224 | public function closestParentOfType($str)
225 | {
226 | $str = strtolower($str);
227 | $currentEl = $this;
228 |
229 | while (strtolower($currentEl->getTagName()) != $str && $currentEl->hasParent()) {
230 | $currentEl = $currentEl->getParent();
231 | }
232 |
233 | if (strtolower($currentEl->getTagName()) != $str) {
234 | return null;
235 | } else {
236 | return $currentEl;
237 | }
238 | }
239 |
240 | }
241 |
--------------------------------------------------------------------------------
/static/scripts/autosize.js:
--------------------------------------------------------------------------------
1 | /*!
2 | Autosize 4.0.0
3 | license: MIT
4 | http://www.jacklmoore.com/autosize
5 | */
6 | (function (global, factory) {
7 | if (typeof define === 'function' && define.amd) {
8 | define(['exports', 'module'], factory);
9 | } else if (typeof exports !== 'undefined' && typeof module !== 'undefined') {
10 | factory(exports, module);
11 | } else {
12 | var mod = {
13 | exports: {}
14 | };
15 | factory(mod.exports, mod);
16 | global.autosize = mod.exports;
17 | }
18 | })(this, function (exports, module) {
19 | 'use strict';
20 |
21 | var map = typeof Map === "function" ? new Map() : (function () {
22 | var keys = [];
23 | var values = [];
24 |
25 | return {
26 | has: function has(key) {
27 | return keys.indexOf(key) > -1;
28 | },
29 | get: function get(key) {
30 | return values[keys.indexOf(key)];
31 | },
32 | set: function set(key, value) {
33 | if (keys.indexOf(key) === -1) {
34 | keys.push(key);
35 | values.push(value);
36 | }
37 | },
38 | 'delete': function _delete(key) {
39 | var index = keys.indexOf(key);
40 | if (index > -1) {
41 | keys.splice(index, 1);
42 | values.splice(index, 1);
43 | }
44 | }
45 | };
46 | })();
47 |
48 | var createEvent = function createEvent(name) {
49 | return new Event(name, { bubbles: true });
50 | };
51 | try {
52 | new Event('test');
53 | } catch (e) {
54 | // IE does not support `new Event()`
55 | createEvent = function (name) {
56 | var evt = document.createEvent('Event');
57 | evt.initEvent(name, true, false);
58 | return evt;
59 | };
60 | }
61 |
62 | function assign(ta) {
63 | if (!ta || !ta.nodeName || ta.nodeName !== 'TEXTAREA' || map.has(ta)) return;
64 |
65 | var heightOffset = null;
66 | var clientWidth = ta.clientWidth;
67 | var cachedHeight = null;
68 |
69 | function init() {
70 | var style = window.getComputedStyle(ta, null);
71 |
72 | if (style.resize === 'vertical') {
73 | ta.style.resize = 'none';
74 | } else if (style.resize === 'both') {
75 | ta.style.resize = 'horizontal';
76 | }
77 |
78 | if (style.boxSizing === 'content-box') {
79 | heightOffset = -(parseFloat(style.paddingTop) + parseFloat(style.paddingBottom));
80 | } else {
81 | heightOffset = parseFloat(style.borderTopWidth) + parseFloat(style.borderBottomWidth);
82 | }
83 | // Fix when a textarea is not on document body and heightOffset is Not a Number
84 | if (isNaN(heightOffset)) {
85 | heightOffset = 0;
86 | }
87 |
88 | update();
89 | }
90 |
91 | function changeOverflow(value) {
92 | {
93 | // Chrome/Safari-specific fix:
94 | // When the textarea y-overflow is hidden, Chrome/Safari do not reflow the text to account for the space
95 | // made available by removing the scrollbar. The following forces the necessary text reflow.
96 | var width = ta.style.width;
97 | ta.style.width = '0px';
98 | // Force reflow:
99 | /* jshint ignore:start */
100 | ta.offsetWidth;
101 | /* jshint ignore:end */
102 | ta.style.width = width;
103 | }
104 |
105 | ta.style.overflowY = value;
106 | }
107 |
108 | function getParentOverflows(el) {
109 | var arr = [];
110 |
111 | while (el && el.parentNode && el.parentNode instanceof Element) {
112 | if (el.parentNode.scrollTop) {
113 | arr.push({
114 | node: el.parentNode,
115 | scrollTop: el.parentNode.scrollTop
116 | });
117 | }
118 | el = el.parentNode;
119 | }
120 |
121 | return arr;
122 | }
123 |
124 | function resize() {
125 | var originalHeight = ta.style.height;
126 | var overflows = getParentOverflows(ta);
127 | var docTop = document.documentElement && document.documentElement.scrollTop; // Needed for Mobile IE (ticket #240)
128 |
129 | ta.style.height = '';
130 |
131 | var endHeight = ta.scrollHeight + heightOffset;
132 |
133 | if (ta.scrollHeight === 0) {
134 | // If the scrollHeight is 0, then the element probably has display:none or is detached from the DOM.
135 | ta.style.height = originalHeight;
136 | return;
137 | }
138 |
139 | ta.style.height = endHeight + 'px';
140 |
141 | // used to check if an update is actually necessary on window.resize
142 | clientWidth = ta.clientWidth;
143 |
144 | // prevents scroll-position jumping
145 | overflows.forEach(function (el) {
146 | el.node.scrollTop = el.scrollTop;
147 | });
148 |
149 | if (docTop) {
150 | document.documentElement.scrollTop = docTop;
151 | }
152 | }
153 |
154 | function update() {
155 | resize();
156 |
157 | var styleHeight = Math.round(parseFloat(ta.style.height));
158 | var computed = window.getComputedStyle(ta, null);
159 |
160 | // Using offsetHeight as a replacement for computed.height in IE, because IE does not account use of border-box
161 | var actualHeight = computed.boxSizing === 'content-box' ? Math.round(parseFloat(computed.height)) : ta.offsetHeight;
162 |
163 | // The actual height not matching the style height (set via the resize method) indicates that
164 | // the max-height has been exceeded, in which case the overflow should be allowed.
165 | if (actualHeight !== styleHeight) {
166 | if (computed.overflowY === 'hidden') {
167 | changeOverflow('scroll');
168 | resize();
169 | actualHeight = computed.boxSizing === 'content-box' ? Math.round(parseFloat(window.getComputedStyle(ta, null).height)) : ta.offsetHeight;
170 | }
171 | } else {
172 | // Normally keep overflow set to hidden, to avoid flash of scrollbar as the textarea expands.
173 | if (computed.overflowY !== 'hidden') {
174 | changeOverflow('hidden');
175 | resize();
176 | actualHeight = computed.boxSizing === 'content-box' ? Math.round(parseFloat(window.getComputedStyle(ta, null).height)) : ta.offsetHeight;
177 | }
178 | }
179 |
180 | if (cachedHeight !== actualHeight) {
181 | cachedHeight = actualHeight;
182 | var evt = createEvent('autosize:resized');
183 | try {
184 | ta.dispatchEvent(evt);
185 | } catch (err) {
186 | // Firefox will throw an error on dispatchEvent for a detached element
187 | // https://bugzilla.mozilla.org/show_bug.cgi?id=889376
188 | }
189 | }
190 | }
191 |
192 | var pageResize = function pageResize() {
193 | if (ta.clientWidth !== clientWidth) {
194 | update();
195 | }
196 | };
197 |
198 | var destroy = (function (style) {
199 | window.removeEventListener('resize', pageResize, false);
200 | ta.removeEventListener('input', update, false);
201 | ta.removeEventListener('keyup', update, false);
202 | ta.removeEventListener('autosize:destroy', destroy, false);
203 | ta.removeEventListener('autosize:update', update, false);
204 |
205 | Object.keys(style).forEach(function (key) {
206 | ta.style[key] = style[key];
207 | });
208 |
209 | map['delete'](ta);
210 | }).bind(ta, {
211 | height: ta.style.height,
212 | resize: ta.style.resize,
213 | overflowY: ta.style.overflowY,
214 | overflowX: ta.style.overflowX,
215 | wordWrap: ta.style.wordWrap
216 | });
217 |
218 | ta.addEventListener('autosize:destroy', destroy, false);
219 |
220 | // IE9 does not fire onpropertychange or oninput for deletions,
221 | // so binding to onkeyup to catch most of those events.
222 | // There is no way that I know of to detect something like 'cut' in IE9.
223 | if ('onpropertychange' in ta && 'oninput' in ta) {
224 | ta.addEventListener('keyup', update, false);
225 | }
226 |
227 | window.addEventListener('resize', pageResize, false);
228 | ta.addEventListener('input', update, false);
229 | ta.addEventListener('autosize:update', update, false);
230 | ta.style.overflowX = 'hidden';
231 | ta.style.wordWrap = 'break-word';
232 |
233 | map.set(ta, {
234 | destroy: destroy,
235 | update: update
236 | });
237 |
238 | init();
239 | }
240 |
241 | function destroy(ta) {
242 | var methods = map.get(ta);
243 | if (methods) {
244 | methods.destroy();
245 | }
246 | }
247 |
248 | function update(ta) {
249 | var methods = map.get(ta);
250 | if (methods) {
251 | methods.update();
252 | }
253 | }
254 |
255 | var autosize = null;
256 |
257 | // Do nothing in Node.js environment and IE8 (or lower)
258 | if (typeof window === 'undefined' || typeof window.getComputedStyle !== 'function') {
259 | autosize = function (el) {
260 | return el;
261 | };
262 | autosize.destroy = function (el) {
263 | return el;
264 | };
265 | autosize.update = function (el) {
266 | return el;
267 | };
268 | } else {
269 | autosize = function (el, options) {
270 | if (el) {
271 | Array.prototype.forEach.call(el.length ? el : [el], function (x) {
272 | return assign(x, options);
273 | });
274 | }
275 | return el;
276 | };
277 | autosize.destroy = function (el) {
278 | if (el) {
279 | Array.prototype.forEach.call(el.length ? el : [el], destroy);
280 | }
281 | return el;
282 | };
283 | autosize.update = function (el) {
284 | if (el) {
285 | Array.prototype.forEach.call(el.length ? el : [el], update);
286 | }
287 | return el;
288 | };
289 | }
290 |
291 | module.exports = autosize;
292 | });
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # blog
2 | This is a simple self-hosted, lightweight, singe-user PHP blog, where you can create your own Facebook-like feed. Give read access to other people, and you can share rich text with photos including highlighted code or links.
3 |
4 | In this context lightweight means:
5 | * No npm dependency, there won't be an annoying 1GB `node_modules` directory.
6 | * No pipeline. What you see is pure code without a need to install it.
7 | * No overhead, essential features, simple usage.
8 |
9 | ## Screenshots
10 |
11 |
15 |
16 | Light theme12 | 13 |  14 |
17 |
21 |
22 | Dark theme18 | 19 |  20 |
23 |
27 |
28 | ## Zero configuration setup
29 | Container will run without any initial configuration needed using SQLite as database provider. For better performance consider using MySQL.
30 |
31 | ```sh
32 | docker run -d -p 80:80 -v $PWD/data:/var/www/html/data m1k1o/blog:latest
33 | ```
34 |
35 | You can set environment variables, prefixed with `BLOG_` and uppercase. They can be found in `config.ini`.
36 | ```sh
37 | docker run -d \
38 | -p 80:80 \
39 | -e "TZ=Europe/Vienna" \
40 | -e "BLOG_TITLE=Blog" \
41 | -e "BLOG_NAME=Max Musermann" \
42 | -e "BLOG_NICK=username" \
43 | -e "BLOG_PASS=password" \
44 | -e "BLOG_LANG=en" \
45 | -v $PWD/data:/var/www/html/data \
46 | m1k1o/blog:latest
47 | ```
48 |
49 | Or for docker-compose format, see [docker-compose.yml](docker-compose.yml).
50 |
51 | ## Install standalone app using `docker-compose` with external database
52 | You need to install [docker-compose](https://docs.docker.com/compose/install/).
53 |
54 | ### MySQL
55 | ```yaml
56 | version: "3"
57 | services:
58 | webserver:
59 | image: m1k1o/blog:latest
60 | container_name: blog_apache
61 | environment:
62 | TZ: Europe/Vienna
63 | BLOG_DB_CONNECTION: mysql
64 | BLOG_MYSQL_HOST: mariadb
65 | BLOG_MYSQL_PORT: 3306
66 | BLOG_MYSQL_USER: blog
67 | BLOG_MYSQL_PASS: blog # use secure password
68 | BLOG_DB_NAME: blog
69 | restart: unless-stopped
70 | ports:
71 | - ${HTTP_PORT-80}:80
72 | volumes:
73 | - ${DATA-./data}:/var/www/html/data
74 | mariadb:
75 | image: mariadb:10.1
76 | container_name: blog_mariadb
77 | environment:
78 | MYSQL_USER: blog
79 | MYSQL_PASSWORD: blog # use secure password
80 | MYSQL_DATABASE: blog
81 | MYSQL_ROOT_PASSWORD: root # use secure password
82 | restart: unless-stopped
83 | volumes:
84 | - mariadb:/var/lib/mysql
85 | - ./app/db/mysql:/docker-entrypoint-initdb.d:ro
86 | volumes:
87 | mariadb:
88 | ```
89 |
90 | ### Postgres
91 | ```yaml
92 | version: "3"
93 | services:
94 | webserver:
95 | image: m1k1o/blog:latest
96 | container_name: blog_apache
97 | environment:
98 | TZ: Europe/Vienna
99 | BLOG_DB_CONNECTION: postgres
100 | BLOG_POSTGRES_HOST: postgres
101 | BLOG_POSTGRES_PORT: 5432
102 | BLOG_POSTGRES_USER: blog
103 | BLOG_POSTGRES_PASS: blog # use secure password
104 | BLOG_DB_NAME: blog
105 | restart: unless-stopped
106 | ports:
107 | - ${HTTP_PORT-80}:80
108 | volumes:
109 | - ${DATA-./data}:/var/www/html/data
110 | postgres:
111 | image: postgres:14
112 | container_name: blog_postgres
113 | environment:
114 | POSTGRES_USER: blog
115 | POSTGRES_PASSWORD: blog # use secure password
116 | POSTGRES_DB: blog
117 | restart: unless-stopped
118 | volumes:
119 | - postgres:/var/lib/postgresql/data
120 | - ./app/db/postgres:/docker-entrypoint-initdb.d:ro
121 | volumes:
122 | postgres:
123 | ```
124 |
125 | ### Step 1: Run `docker-compose.yml`.
126 | Select one of configurations above and save it to `docker-compose.yml`. Then run:
127 | ```sh
128 | docker-compose up -d
129 | ```
130 |
131 | You can specify these environment variables, otherwise the default ones will be used:
132 | * **HTTP_PORT=80** - where the blog will be accessible.
133 | * **DATA=./data** - directory to store the user data.
134 |
135 | These environment variables can be stored in the `.env` file or passed to the command directly:
136 | ```sh
137 | HTTP_PORT=3001 DATA=/home/user/blog docker-compose up -d
138 | ```
139 |
140 | ### Step 2: Create `data/` directory and download `config.ini` file.
141 | Download default config file and copy to your new `./data/` directory.
142 |
143 | ```sh
144 | mkdir data && cd data
145 | wget https://raw.githubusercontent.com/m1k1o/blog/master/config.ini
146 | ```
147 |
148 | Now you can modify your config. Or you can set environment variables, in uppercase, starting with `BLOG_`, e.g. `BLOG_NAME: Max's blog`.
149 |
150 | ### Correct permissions
151 | Make sure your `./data/` directory has correct permissions. Apache is running as a `www-data` user, which needs to have write access to the `./data/` directory (for uploading images).
152 |
153 | #### Prefered solution
154 | Change the directory owner to the `www-data` user:
155 |
156 | ```sh
157 | chown 33:33 ./data/
158 | ```
159 |
160 | Alternatively, add the `www-data` user to the user group that owns the `./data/` directory.
161 |
162 | #### Bad solution (but it works)
163 | Set `777` permission for your `./data/`, so everyone can read, write, and execute:
164 |
165 | ```sh
166 | chmod 777 ./data/
167 | ```
168 |
169 | **NOTICE:** You should not use `777`. You are giving access to anyone for this directory. Maybe to some attacker, who can run his exploit here.
170 |
171 | ## Install
172 | If you have decided that you don't want to use Docker, you can intall it manually.
173 |
174 | **Requirements:** Apache 2.0*, PHP 7.4, (MariaDB 10.1 or SQLite 3)
175 |
176 | **NOTICE:** If you would like to use Nginx or another web server, make sure that the sensitive data are not exposed to the public. Since `.htaccess` is protecting those files in Apache, that could not be the case in a different environment. Take care of:
177 | * **config.ini** - disallow access to all *.ini* files for the public.
178 | * **data/logs/\_ANY_.log** - make sure no sensitive information are located in *.log*.
179 |
180 | ### Database Schema
181 | You can find database schema in `./app/db` folder.
182 |
183 | ### Debug mode
184 | To check if your server is set up correctly, turn on a debug mode (in config add `debug = true`) to see the details. In the debug mode, an error may be shown if you are missing some **PHP extensions** needed to be installed on your server.
185 |
186 | ## Config file
187 | **DO NOT** edit `./config.ini` file. If you wish to modify the config, simply make a copy to the `./data/config.ini` directory and edit it there.
188 |
189 | **But, why?** If there is any change in config file (most likely adding a new feature), you will have problems with merging a new version. Also, if you would fork this repository, you might accidentally push your secrets to the git. We don't want that to happen. Content of the `/data` directory is ignored by the git, so none of your pictures or personal data should ever be published to git.
190 |
191 | # Features
192 |
193 | * Dark mode, retina ready, legacy theme available.
194 | * Use BBcode in texts.
195 | * Make posts available for **everyone**, **only you** or just for **friends**.
196 | * Extra fields in post: **Feeling**, **With** and **At**.
197 | * Hide posts from timeline so they are visible only when you need them to be.
198 | * All pasted links will get preview with page title, description and image (can be configured proxy).
199 | * Upload images using button *(for mobile)*.
200 | * Upload images using drag & drop *(drop it into textarea)*.
201 | * Upload images using CTRL + V *(paste it into textarea)*.
202 | * Highlight code in post using `[code]..your code..[/code]`.
203 | * Highlight your goal using `[goal]Text of your goal.[/goal]`.
204 | * Use tags in posts (allowed characters `A-Za-z0-9-_` terminated by space or EOL): `#song`.
205 | * Sort posts in reverse order (oldest first): `http://blog/#sort=reverse`.
206 | * Filter posts by hashtags: `http://blog/#tag=songs`.
207 | * Filter posts by location in url using: `http://blog/#loc=Vienna`.
208 | * Display posts from chosen date using (format YYYY-MM-DD or YYY-MM): `http://blog/#from=2017-06`.
209 | * Display posts to chosen date using (format YYYY-MM-DD or YYY-MM): `http://blog/#to=2017-06`.
210 | * Combine parameters in url using `&`, e.g. show posts between dates: `http://blog/#from=2017-06&to=2017-08`.
211 |
212 | ## Access control
213 |
214 | This blog is using Mandatory Access Control (MAC), with 3 types of access levels:
215 |
216 | * **Private** posts are visible only to your single account specified in `nick` and `pass`.
217 | * You can specify group of your **friends** and share posts only for them.
218 | * **Public** posts are visible to everyone, without login.
219 |
220 | In `docker-compose.yml` file, specify your credentials and friends like this:
221 |
222 | ```yml
223 | version: "3"
224 | services:
225 | blog:
226 | image: m1k1o/blog:latest
227 | restart: unless-stopped
228 | environment:
229 | TZ: Europe/Vienna
230 | BLOG_NICK: admin_username
231 | BLOG_PASS: admin_password
232 | BLOG_FRIENDS: |
233 | jane:mysecretpass
234 | thomas:anotherpass
235 | ports:
236 | - 80:80
237 | volumes:
238 | - ./data:/var/www/html/data
239 | ```
240 |
241 | You can specify your credentials and friends in your `config.ini` file e.g.:
242 |
243 | ```ini
244 | [admin]
245 | force_login = true
246 | nick = admin_username
247 | pass = admin_password
248 |
249 | [friends]
250 | friends[jane] = mysecretpass
251 | friends[thomas] = anotherpass
252 | ```
253 |
254 | ## Localisation
255 | Timezone can be set in config or, for docker users, `TZ` environment variable is supported. List of timezones can be found [here](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones).
256 |
257 | ### Language support
258 | Feel free to create new PR and add a new language. Specify language in config or in url: `http://blog/?hl=sk`.
259 |
260 | * en - 🇬🇧 English
261 | * de - 🇩🇪 German
262 | * sk - 🇸🇰 Slovak
263 | * fr - 🇫🇷 French (thanks @Phundrak)
264 | * cz - 🇨🇿 Czech (thanks @djfinch)
265 | * bs - 🇧🇦 Bosnian (thanks @hajro92)
266 | * es - 🇪🇸 Spanish (thanks @ManuLinares)
267 | * ru - 🇷🇺 Russian (thanks @ozzyst)
268 |
--------------------------------------------------------------------------------
/app/jbbcode/codedefinition.class.php:
--------------------------------------------------------------------------------
1 | elCounter = 0;
48 | $def->setTagName($tagName);
49 | $def->setReplacementText($replacementText);
50 | $def->useOption = $useOption;
51 | $def->parseContent = $parseContent;
52 | $def->nestLimit = $nestLimit;
53 | $def->optionValidator = $optionValidator;
54 | $def->bodyValidator = $bodyValidator;
55 | return $def;
56 | }
57 |
58 | /**
59 | * Constructs a new CodeDefinition.
60 | *
61 | * This constructor is deprecated. You should use the static construct() method or the
62 | * CodeDefinitionBuilder class to construct a new CodeDefiniton.
63 | *
64 | * @deprecated
65 | */
66 | public function __construct()
67 | {
68 | /* WARNING: This function is deprecated and will be made protected in a future
69 | * version of jBBCode. */
70 | $this->parseContent = true;
71 | $this->useOption = false;
72 | $this->nestLimit = -1;
73 | $this->elCounter = 0;
74 | $this->optionValidator = array();
75 | $this->bodyValidator = null;
76 | }
77 |
78 | /**
79 | * Determines if the arguments to the given element are valid based on
80 | * any validators attached to this CodeDefinition.
81 | *
82 | * @param $el the ElementNode to validate
83 | * @return true if the ElementNode's {option} and {param} are OK, false if they're not
84 | */
85 | public function hasValidInputs(ElementNode $el)
86 | {
87 | if ($this->usesOption() && $this->optionValidator) {
88 | $att = $el->getAttribute();
89 |
90 | foreach($att as $name => $value){
91 | if(isset($this->optionValidator[$name]) && !$this->optionValidator[$name]->validate($value)){
92 | return false;
93 | }
94 | }
95 | }
96 |
97 | if (!$this->parseContent() && $this->bodyValidator) {
98 | /* We only evaluate the content if we're not parsing the content. */
99 | $content = "";
100 | foreach ($el->getChildren() as $child) {
101 | $content .= $child->getAsBBCode();
102 | }
103 | if (!$this->bodyValidator->validate($content)) {
104 | /* The content of the element is not valid. */
105 | return false;
106 | }
107 | }
108 |
109 | return true;
110 | }
111 |
112 | /**
113 | * Accepts an ElementNode that is defined by this CodeDefinition and returns the HTML
114 | * markup of the element. This is a commonly overridden class for custom CodeDefinitions
115 | * so that the content can be directly manipulated.
116 | *
117 | * @param $el the element to return an html representation of
118 | *
119 | * @return the parsed html of this element (INCLUDING ITS CHILDREN)
120 | */
121 | public function asHtml(ElementNode $el)
122 | {
123 | if (!$this->hasValidInputs($el)) {
124 | return $el->getAsBBCode();
125 | }
126 |
127 | $html = $this->getReplacementText();
128 |
129 | if ($this->usesOption()) {
130 | $options = $el->getAttribute();
131 | if(count($options)==1){
132 | $vals = array_values($options);
133 | $html = str_ireplace('{option}', reset($vals), $html);
134 | }
135 | else{
136 | foreach($options as $key => $val){
137 | $html = str_ireplace('{' . $key . '}', $val, $html);
138 | }
139 | }
140 | }
141 |
142 | $content = $this->getContent($el);
143 |
144 | $html = str_ireplace('{param}', $content, $html);
145 |
146 | return $html;
147 | }
148 |
149 | protected function getContent(ElementNode $el){
150 | if ($this->parseContent()) {
151 | $content = "";
152 | foreach ($el->getChildren() as $child)
153 | $content .= $child->getAsHTML();
154 | } else {
155 | $content = "";
156 | foreach ($el->getChildren() as $child)
157 | $content .= $child->getAsBBCode();
158 | }
159 | return $content;
160 | }
161 |
162 | /**
163 | * Accepts an ElementNode that is defined by this CodeDefinition and returns the text
164 | * representation of the element. This may be overridden by a custom CodeDefinition.
165 | *
166 | * @param $el the element to return a text representation of
167 | *
168 | * @return the text representation of $el
169 | */
170 | public function asText(ElementNode $el)
171 | {
172 | if (!$this->hasValidInputs($el)) {
173 | return $el->getAsBBCode();
174 | }
175 |
176 | $s = "";
177 | foreach ($el->getChildren() as $child)
178 | $s .= $child->getAsText();
179 | return $s;
180 | }
181 |
182 | /**
183 | * Returns the tag name of this code definition
184 | *
185 | * @return this definition's associated tag name
186 | */
187 | public function getTagName()
188 | {
189 | return $this->tagName;
190 | }
191 |
192 | /**
193 | * Returns the replacement text of this code definition. This usually has little, if any meaning if the
194 | * CodeDefinition class was extended. For default, html replacement CodeDefinitions this returns the html
195 | * markup for the definition.
196 | *
197 | * @return the replacement text of this CodeDefinition
198 | */
199 | public function getReplacementText()
200 | {
201 | return $this->replacementText;
202 | }
203 |
204 | /**
205 | * Returns whether or not this CodeDefinition uses the optional {option}
206 | *
207 | * @return true if this CodeDefinition uses the option, false otherwise
208 | */
209 | public function usesOption()
210 | {
211 | return $this->useOption;
212 | }
213 |
214 | /**
215 | * Returns whether or not this CodeDefnition parses elements contained within it,
216 | * or just treats its children as text.
217 | *
218 | * @return true if this CodeDefinition parses elements contained within itself
219 | */
220 | public function parseContent()
221 | {
222 | return $this->parseContent;
223 | }
224 |
225 | /**
226 | * Returns the limit of how many elements defined by this CodeDefinition may be
227 | * nested together. If after parsing elements are nested beyond this limit, the
228 | * subtrees formed by those nodes will be removed from the parse tree. A nest
229 | * limit of -1 signifies no limit.
230 | */
231 | public function getNestLimit()
232 | {
233 | return $this->nestLimit;
234 | }
235 |
236 | /**
237 | * Sets the tag name of this CodeDefinition
238 | *
239 | * @deprecated
240 | *
241 | * @param the new tag name of this definition
242 | */
243 | public function setTagName($tagName)
244 | {
245 | $this->tagName = strtolower($tagName);
246 | }
247 |
248 | /**
249 | * Sets the html replacement text of this CodeDefinition
250 | *
251 | * @deprecated
252 | *
253 | * @param the new replacement text
254 | */
255 | public function setReplacementText($txt)
256 | {
257 | $this->replacementText = $txt;
258 | }
259 |
260 | /**
261 | * Sets whether or not this CodeDefinition uses the {option}
262 | *
263 | * @deprecated
264 | *
265 | * @param boolean $bool
266 | */
267 | public function setUseOption($bool)
268 | {
269 | $this->useOption = $bool;
270 | }
271 |
272 | /**
273 | * Sets whether or not this CodeDefinition allows its children to be parsed as html
274 | *
275 | * @deprecated
276 | *
277 | * @param boolean $bool
278 | */
279 | public function setParseContent($bool)
280 | {
281 | $this->parseContent = $bool;
282 | }
283 |
284 | /**
285 | * Increments the element counter. This is used for tracking depth of elements of the same type for next limits.
286 | *
287 | * @deprecated
288 | *
289 | * @return void
290 | */
291 | public function incrementCounter()
292 | {
293 | $this->elCounter++;
294 | }
295 |
296 | /**
297 | * Decrements the element counter.
298 | *
299 | * @deprecated
300 | *
301 | * @return void
302 | */
303 | public function decrementCounter()
304 | {
305 | $this->elCounter--;
306 | }
307 |
308 | /**
309 | * Resets the element counter.
310 | *
311 | * @deprecated
312 | */
313 | public function resetCounter()
314 | {
315 | $this->elCounter = 0;
316 | }
317 |
318 | /**
319 | * Returns the current value of the element counter.
320 | *
321 | * @deprecated
322 | *
323 | * @return int
324 | */
325 | public function getCounter()
326 | {
327 | return $this->elCounter;
328 | }
329 | }
330 |
--------------------------------------------------------------------------------
/app/db.class.php:
--------------------------------------------------------------------------------
1 | mysql_connect();
44 | break;
45 | case 'postgres':
46 | $this->postgres_connect();
47 | break;
48 | case 'sqlite':
49 | $this->sqlite_connect();
50 | break;
51 | }
52 | }
53 |
54 | private final function mysql_connect(){
55 | $host = Config::get_safe('mysql_host', false);
56 | $port = Config::get_safe('mysql_port', false);
57 | $socket = Config::get_safe('mysql_socket', false);
58 |
59 | if($socket === false && $host === false){
60 | throw new DBException("Mysql host or socket must be defined.");
61 | }
62 |
63 | // Try to connect
64 | try {
65 | $this->_PDO = new \PDO(
66 | // Server
67 | 'mysql:'.
68 | ($socket !== false
69 | ? 'unix_socket='.$socket
70 | : 'host='.$host.($port !== false ? ';port='.$port : '')
71 | ).
72 | // DB
73 | ';dbname='.Config::get('db_name').
74 | // Charset
75 | ';charset=utf8',
76 | // Username
77 | Config::get('mysql_user'),
78 | // Password
79 | Config::get_safe('mysql_pass', ''),
80 | // Set attributes
81 | [
82 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
83 | \PDO::ATTR_EMULATE_PREPARES => false
84 | ]
85 | );
86 |
87 | $this->_PDO->exec(
88 | // Set charset
89 | 'SET NAMES utf8;'.
90 |
91 | // Set timezone
92 | 'SET time_zone="'.date('P').'";'
93 | );
94 | } catch (PDOException $e) {
95 | throw new DBException($e->getMessage());
96 | }
97 | }
98 |
99 | private final function postgres_connect(){
100 | $host = Config::get_safe('postgres_host', false);
101 | $port = Config::get_safe('postgres_port', false);
102 | $socket = Config::get_safe('postgres_socket', false);
103 |
104 | if($socket === false && $host === false){
105 | throw new DBException("Postgres host or socket must be defined.");
106 | }
107 |
108 | // Try to connect
109 | try {
110 | $this->_PDO = new \PDO(
111 | // Server
112 | 'pgsql:'.
113 | ($socket !== false
114 | ? 'unix_socket='.$socket
115 | : 'host='.$host.($port !== false ? ';port='.$port : '')
116 | ).
117 | // DB
118 | ';dbname='.Config::get('db_name').
119 | // Charset
120 | ';options=\'--client_encoding=UTF8\'',
121 | // Username
122 | Config::get('postgres_user'),
123 | // Password
124 | Config::get_safe('postgres_pass', ''),
125 | // Set attributes
126 | [
127 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION,
128 | \PDO::ATTR_EMULATE_PREPARES => false
129 | ]
130 | );
131 |
132 | $this->_PDO->exec(
133 | // Set timezone
134 | 'SET TIME ZONE "'.date('e').'";'
135 | );
136 | } catch (PDOException $e) {
137 | throw new DBException($e->getMessage());
138 | }
139 | }
140 |
141 | private final function sqlite_connect(){
142 | $sqlite_db = PROJECT_PATH.Config::get_safe('sqlite_db', "data/sqlite.db");
143 |
144 | // First run of sqlite
145 | if(!file_exists($sqlite_db)) {
146 | if(!is_writable(dirname($sqlite_db))) {
147 | throw new DBException("Sqlite database directory must me writable.");
148 | }
149 |
150 | if(!touch($sqlite_db)) {
151 | throw new DBException("Cannot create sqlite database file.");
152 | }
153 |
154 | // Inilialize SQL schema
155 | $sql_schema = file_get_contents(APP_PATH."db/sqlite/01_schema.sql");
156 |
157 | try {
158 | $this->_PDO = new \PDO("sqlite:".$sqlite_db, null, null, [
159 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
160 | ]);
161 | $this->_PDO->exec($sql_schema);
162 | } catch (PDOException $e) {
163 | $this->_PDO = null;
164 | unlink($sqlite_db);
165 |
166 | throw new DBException($e->getMessage());
167 | }
168 |
169 | return ;
170 | }
171 |
172 | // Try to connect
173 | try {
174 | $this->_PDO = new \PDO("sqlite:".$sqlite_db, null, null, [
175 | \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION
176 | ]);
177 | } catch (PDOException $e) {
178 | throw new DBException($e->getMessage());
179 | }
180 | }
181 |
182 | // Just flattern array to be binded : [key1, key2, [key3, [key4]]] => [key1, key2, key3, key4]
183 | private final function bind_value($key, $value){
184 | if(is_array($value)){
185 | foreach($value as $one_value){
186 | $key = $this->bind_value($key, $one_value);
187 | }
188 |
189 | return $key;
190 | }
191 |
192 | // BUG: Force strings to be UTF-8
193 | // remove all 4-bytes characters.
194 | if(is_string($value)){
195 | $value = preg_replace('/[\xF0-\xF7].../s', '', $value);
196 | }
197 |
198 | $this->_query->bindValue($key, $value);
199 | return ++$key;
200 | }
201 |
202 | // Process Query
203 | // query ($sql)
204 | // query ($sql, $bind_param_01, $bind_param_02, ...)
205 | // query ($sql, [$bind_param_01, $bind_param_02, ...])
206 | public final function query(){
207 | // Second parm is binded values
208 | $params = func_get_args();
209 |
210 | // First parameter is sql
211 | $sql = $params[0];
212 | unset($params[0]);
213 |
214 | // Replace backticks with " for postgres
215 | if(DB::connection() === 'postgres') {
216 | $sql = str_replace("`", '"', $sql);
217 | }
218 |
219 | // Debug mode
220 | if(Config::get_safe('debug', false)){
221 | echo "\n";
222 | }
223 |
224 | // Try to prepare MySQL statement
225 | try {
226 | // Prepare PDO statement
227 | $this->_query = $this->_PDO->prepare($sql);
228 |
229 | // Bind values
230 | $this->bind_value(1, $params);
231 |
232 | // Execute
233 | $this->_query->execute();
234 | } catch (PDOException $e) {
235 | throw new DBException($e->getMessage());
236 | }
237 |
238 | $this->_query_counter++;
239 | return $this;
240 | }
241 |
242 | // Insert into table
243 | public final function insert($table_name, $fields = null){
244 | // If empty line
245 | if(empty($fields)){
246 | return $this->query("INSERT INTO `{$table_name}` () VALUES ()");
247 | }
248 |
249 | // If multiple
250 | if(isset($fields[0])){
251 | // Turn array into PDO prepered statement format
252 | $keys = array_keys($fields[0]);
253 |
254 | // Build query
255 | $query = "INSERT INTO `{$table_name}` (`".implode('`, `', $keys)."`) VALUES ";
256 |
257 | // Insert values
258 | $first = true;
259 | $prepared_data = array();
260 | foreach($fields as $field){
261 | if($first){
262 | $first = false;
263 | } else {
264 | $query .= ',';
265 | }
266 |
267 | end($field);
268 | $last_key = key($field);
269 |
270 | $query .= '(';
271 | foreach($field as $key => $value){
272 | if($value === "NOW()"){
273 | if(DB::connection() === 'sqlite') {
274 | $query .= "datetime('now', 'localtime')";
275 | } else {
276 | $query .= "NOW()";
277 | }
278 | } else {
279 | $query .= '?';
280 | $prepared_data[] = $value;
281 | }
282 |
283 | if($last_key != $key){
284 | $query .= ',';
285 | }
286 | }
287 | $query .= ')';
288 | }
289 |
290 | // Execute query
291 | return $this->query($query, $prepared_data);
292 | }
293 |
294 | // If only single
295 | return $this->insert($table_name, array($fields));
296 | }
297 |
298 | // Update table
299 | // update ($table_name, $fields)
300 | // update ($table_name, $fields, $sql)
301 | // update ($table_name, $fields, $sql, $bind_param_01, $bind_param_02, ...)
302 | // update ($table_name, $fields, $sql, [$bind_param_01, $bind_param_02, ...])
303 | public final function update(){
304 | // Fourt param is binded values
305 | $params = func_get_args();
306 |
307 | // First is table_name
308 | $table_name = $params[0];
309 | unset($params[0]);
310 |
311 | // Second is fields
312 | $fields = $params[1];
313 | unset($params[1]);
314 |
315 | // Third is sql
316 | $sql = $params[2];
317 | unset($params[2]);
318 |
319 | // If fields are not array, do nothing
320 | if(!is_array($fields)){
321 | return $this;
322 | }
323 |
324 | end($fields);
325 | $last_key = key($fields);
326 |
327 | // Support for NOW()
328 | $prepared_data = array();
329 | $set_data = null;
330 | foreach($fields as $key => $value){
331 | if($value === "NOW()"){
332 | if(DB::connection() === 'sqlite') {
333 | $set_data .="`{$key}` = datetime('now', 'localtime')";
334 | } else {
335 | $set_data .="`{$key}` = NOW()";
336 | }
337 | } else {
338 | $set_data .= "`{$key}` = ?";
339 | $prepared_data[] = $value;
340 | }
341 |
342 | if($last_key != $key){
343 | $set_data .= ',';
344 | }
345 | }
346 |
347 | // If params are not array, make it
348 | if(!is_array($params)){
349 | $params = array($params);
350 | }
351 |
352 | // Merge fields array and additional SQL data
353 | foreach($params as $param){
354 | $prepared_data[] = $param;
355 | }
356 |
357 | // Build query
358 | $query = "UPDATE `{$table_name}` SET {$set_data} ".$sql;
359 |
360 | // Execute query
361 | return $this->query($query, $prepared_data);
362 | }
363 |
364 | // Alias for all
365 | public final function results(){
366 | trigger_error("Using deprecated method DB::results();. Use DB::all(); instead.");
367 | return $this->all();
368 | }
369 |
370 | // Get all rows
371 | public final function all($type = \PDO::FETCH_ASSOC){
372 | return $this->_query->fetchAll($type);
373 | }
374 |
375 | // Get all values to one dimensional array
376 | public final function columns($column = 0){
377 | return $this->_query->fetchAll(\PDO::FETCH_COLUMN, $column);
378 | }
379 |
380 | // Get first row from result
381 | public final function first($key = null){
382 | $results = $this->all();
383 |
384 | if($key !== null){
385 | return @$results[0][$key];
386 | }
387 |
388 | return @$results[0];
389 | }
390 |
391 | // Get last inserted ID
392 | public final function last_id(){
393 | return $this->_PDO->lastInsertId();
394 | }
395 |
396 | // Exec
397 | public final function exec($sql){
398 | // Try to execute MySQL
399 | try {
400 | $this->_PDO->exec($sql);
401 | } catch (PDOException $e) {
402 | throw new DBException($e->getMessage());
403 | }
404 |
405 | return $this;
406 | }
407 |
408 | public final function total_queries(){
409 | return $this->_query_counter;
410 | }
411 | }
412 |
413 | // Handle DB errors
414 | class DBException extends Exception{}
--------------------------------------------------------------------------------
/app/post.class.php:
--------------------------------------------------------------------------------
1 | addCodeDefinitionSet(new JBBCode\DefaultCodeDefinitionSet());
15 |
16 | if(Config::get("highlight")){
17 | $c = str_replace("\t", " ", $c);
18 | $c = preg_replace("/\[code(?:=([^\[]+))?\]\s*?(?:\n|\r)?/i", '[code=$1]', $c);
19 | $c = preg_replace("/\[\/code\]\s*?(?:\n|\r)?/i", '[/code]', $c);
20 |
21 | // Add code definiton
22 | $parser->addCodeDefinition(new class extends \JBBCode\CodeDefinition {
23 | public function __construct(){
24 | parent::__construct();
25 | $this->setTagName("code");
26 | $this->setParseContent(false);
27 | $this->setUseOption(true);
28 | }
29 |
30 | public function asHtml(\JBBCode\ElementNode $el){
31 | $content = $this->getContent($el);
32 | $class = $el->getAttribute()['code'];
33 | return 'Legacy theme (compatible with older browsers)24 | 25 |  26 |'.htmlentities($content).'';
34 | }
35 | });
36 | }
37 |
38 | // Custom tags
39 | $builder = new JBBCode\CodeDefinitionBuilder("goal", "{param} ");
40 | $parser->addCodeDefinition($builder->build());
41 |
42 | $builder = new JBBCode\CodeDefinitionBuilder("goal", "{param} ");
43 | $builder->setUseOption(true);
44 | $parser->addCodeDefinition($builder->build());
45 |
46 | if(($tags = Config::get_safe("bbtags", [])) && !empty($tags)){
47 | foreach($tags as $tag => $content){
48 | $builder = new JBBCode\CodeDefinitionBuilder($tag, $content);
49 | $parser->addCodeDefinition($builder->build());
50 | }
51 | }
52 |
53 | $parser->parse($c);
54 |
55 | // Visit every text node
56 | $parser->accept(new class implements \JBBCode\NodeVisitor{
57 | function visitDocumentElement(\JBBCode\DocumentElement $documentElement){
58 | foreach($documentElement->getChildren() as $child) {
59 | $child->accept($this);
60 | }
61 | }
62 |
63 | function visitTextNode(\JBBCode\TextNode $textNode){
64 | $c = $textNode->getValue();
65 | $c = preg_replace('/\"([^\"]+)\"/i', "„$1\"", $c);
66 | $c = htmlentities($c);
67 | $c = preg_replace('/\*([^\*]+)\*/i', "$1", $c);
68 | $c = preg_replace('/(https?\:\/\/[^\" \n]+)/i', "\\0", $c);
69 | $c = preg_replace('/(\#[A-Za-z0-9-_]+)(\s|$)/i', "\\1\\2", $c);
70 | $c = nl2br($c);
71 | $textNode->setValue($c);
72 | }
73 |
74 | function visitElementNode(\JBBCode\ElementNode $elementNode){
75 | /* We only want to visit text nodes within elements if the element's
76 | * code definition allows for its content to be parsed.
77 | */
78 | if ($elementNode->getCodeDefinition()->parseContent()) {
79 | foreach ($elementNode->getChildren() as $child) {
80 | $child->accept($this);
81 | }
82 | }
83 | }
84 | });
85 |
86 | return $parser->getAsHtml();
87 | }
88 |
89 | private static function raw_data($raw_input){
90 | $default_input = [
91 | "text" => '',
92 | "plain_text" => '',
93 | "feeling" => '',
94 | "persons" => '',
95 | "location" => '',
96 | "content_type" => '',
97 | "content" => '',
98 | "privacy" => ''
99 | ];
100 |
101 | // Handle only allowed keys
102 | $raw_output = array();
103 | foreach($default_input as $key => $def){
104 | // Key exists in input
105 | if(array_key_exists($key, $raw_input)){
106 | $raw_output[$key] = $raw_input[$key];
107 | } else {
108 | $raw_output[$key] = $default_input[$key];
109 | }
110 | }
111 |
112 | if($raw_output['privacy'] != "public" && $raw_output['privacy'] != "friends"){
113 | $raw_output['privacy'] = "private";
114 | }
115 |
116 | return $raw_output;
117 | }
118 |
119 | public static function insert($r){
120 | self::login_protected();
121 |
122 | $data = self::raw_data($r);
123 |
124 | if(empty($data['text'])){
125 | throw new Exception(__("No data."));
126 | }
127 |
128 | $data['plain_text'] = $data['text'];
129 | $data['text'] = self::parse_content($data['text']);
130 | $data['datetime'] = 'NOW()';
131 | $data['status'] = '1';
132 |
133 | $data['id'] = DB::get_instance()->insert('posts', $data)->last_id();
134 |
135 | $data['datetime'] = date("d M Y H:i");
136 | unset($data['plain_text']);
137 |
138 | return $data;
139 | }
140 |
141 | public static function update($r){
142 | self::login_protected();
143 |
144 | $data = self::raw_data($r);
145 |
146 | $data['plain_text'] = $data['text'];
147 | $data['text'] = self::parse_content($data['text']);
148 |
149 | DB::get_instance()->update('posts', $data, "WHERE `id` = ? AND `status` <> 5", $r["id"]);
150 |
151 | unset($data['plain_text']);
152 |
153 | return $data;
154 | }
155 |
156 | public static function hide($r){
157 | self::login_protected();
158 |
159 | DB::get_instance()->query("
160 | UPDATE `posts`
161 | SET `status` = 4
162 | WHERE `id` = ?
163 | AND `status` <> 5
164 | ", $r["id"]);
165 | return true;
166 | }
167 |
168 | public static function show($r){
169 | self::login_protected();
170 |
171 | DB::get_instance()->query("
172 | UPDATE `posts`
173 | SET `status` = 1
174 | WHERE `id` = ?
175 | AND `status` <> 5
176 | ", $r["id"]);
177 | return true;
178 | }
179 |
180 | public static function delete($r){
181 | self::login_protected();
182 |
183 | DB::get_instance()->query("
184 | UPDATE `posts`
185 | SET `status` = 5
186 | WHERE `id` = ?
187 | ", $r["id"]);
188 | return true;
189 | }
190 |
191 | public static function edit_data($r){
192 | self::login_protected();
193 |
194 | return DB::get_instance()->query("
195 | SELECT `plain_text`, `feeling`, `persons`, `location`, `privacy`, `content_type`, `content`
196 | FROM `posts`
197 | WHERE `id` = ?
198 | AND `status` <> 5
199 | ", $r["id"])->first();
200 | }
201 |
202 | public static function get_date($r){
203 | self::login_protected();
204 |
205 | if (DB::connection() === 'sqlite') {
206 | $datetime = "strftime('%Y %m %d %H %M', `posts`.`datetime`)";
207 | } else if (DB::connection() === 'postgres') {
208 | $datetime = "to_char(datetime,'YYYY MM DD HH24 MI')";
209 | } else {
210 | $datetime = "DATE_FORMAT(`datetime`,'%Y %c %e %k %i')";
211 | }
212 |
213 | $date = DB::get_instance()->query("
214 | SELECT $datetime AS `date_format`
215 | FROM `posts`
216 | WHERE `id` = ?
217 | AND `status` <> 5
218 | ", $r["id"])->first("date_format");
219 | $date = array_map("intval", explode(" ", $date));
220 | $date[4] = floor($date[4]/10)*10;
221 | return $date;
222 | }
223 |
224 | public static function set_date($r){
225 | self::login_protected();
226 |
227 | $d = $r["date"];
228 | if (DB::connection() === 'sqlite') {
229 | $datetime = vsprintf("%04d-%02d-%02d %02d:%02d", $d);
230 | } else {
231 | $datetime = vsprintf("%04d/%02d/%02d %02d:%02d", $d);
232 | }
233 |
234 | DB::get_instance()->query("
235 | UPDATE `posts`
236 | SET `datetime` = ?
237 | WHERE `id` = ?
238 | AND `status` <> 5
239 | ", $datetime, $r["id"]);
240 | return [ "datetime" => date("d M Y H:i", strtotime($datetime)) ];
241 | }
242 |
243 | public static function parse_link($r){
244 | self::login_protected();
245 |
246 | $l = $r["link"];
247 |
248 | preg_match('/^https?:\/\/([^:\/\s]+)([^\/\s]*\/)([^\.\s]+)\.(jpe?g|png|gif)((\?|\#)(.*))?$/i', $l, $img);
249 | if($img){
250 | return [
251 | "valid" => true,
252 | "content_type" => "img_link",
253 | "content" => [
254 | "src" => $l,
255 | "host" => $img[1]
256 | ]
257 | ];
258 | }
259 |
260 | preg_match('/^https?:\/\/(www\.)?([^:\/\s]+)(.*)?$/i', $l, $url);
261 | $curl_request_url = $l;
262 |
263 | // Get content
264 | $ch = curl_init();
265 | curl_setopt($ch, CURLOPT_HEADER, 0);
266 | curl_setopt($ch, CURLOPT_ENCODING , "");
267 | curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
268 | curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
269 | curl_setopt($ch, CURLOPT_USERAGENT, "Mozilla/5.0 (compatible; Proxycat/1.1)");
270 | curl_setopt($ch, CURLOPT_REFERER, '');
271 | curl_setopt($ch, CURLOPT_TIMEOUT, 7); // 7sec
272 |
273 | // Proxy settings
274 | if($proxy = Config::get_safe("proxy", false)){
275 | $proxytype = Config::get_safe("proxytype", false);
276 | $proxyauth = Config::get_safe("proxyauth", false);
277 | if($proxytype === 'URL_PREFIX'){
278 | $curl_request_url = $proxy.$curl_request_url;
279 |
280 | if($proxyauth){
281 | curl_setopt($ch, CURLOPT_USERPWD, $proxyauth);
282 | }
283 | } else {
284 | curl_setopt($ch, CURLOPT_PROXY, $proxy);
285 |
286 | if($proxyauth){
287 | curl_setopt($ch, CURLOPT_PROXYUSERPWD, $proxyauth);
288 | }
289 |
290 | switch ($proxytype) {
291 | case 'CURLPROXY_SOCKS4':
292 | $proxytype = CURLPROXY_SOCKS4;
293 | break;
294 | case 'CURLPROXY_SOCKS5':
295 | $proxytype = CURLPROXY_SOCKS5;
296 | break;
297 | case 'CURLPROXY_HTTP':
298 | default:
299 | $proxytype = CURLPROXY_HTTP;
300 | break;
301 | }
302 |
303 | curl_setopt($ch, CURLOPT_PROXYTYPE, $proxytype);
304 | }
305 | }
306 |
307 | curl_setopt($ch, CURLOPT_URL, $curl_request_url);
308 | $html = curl_exec($ch);
309 | curl_close($ch);
310 |
311 | // Parse
312 | $doc = new DOMDocument();
313 | @$doc->loadHTML(''.$html);
314 |
315 | // Get title
316 | $nodes = $doc->getElementsByTagName('title');
317 | $title = $nodes->item(0)->nodeValue;
318 |
319 | // Content
320 | $content = [
321 | "link" => $l,
322 | "title" => ($title ? $title : $url[2]),
323 | "is_video" => false,
324 | "host" => $url[2]
325 | ];
326 |
327 | // Metas
328 | $metas = $doc->getElementsByTagName('meta');
329 | for($i = 0; $i < $metas->length; $i++){
330 | $meta = $metas->item($i);
331 |
332 | $n = $meta->getAttribute('name');
333 | $p = $meta->getAttribute('property');
334 | $c = $meta->getAttribute('content');
335 |
336 | if($n == 'twitter:description' || $p == 'og:description' || $n == 'description'){
337 | $content["desc"] = substr($c, 0, 180);
338 | }
339 |
340 | if($n == 'twitter:title' || $p == 'og:title' || $p == 'title'){
341 | $content["title"] = $c;
342 | }
343 |
344 | if($p == 'og:url'){
345 | $content["link"] = $c;
346 | }
347 |
348 | if($p == 'og:type'){
349 | $content["is_video"] = (preg_match("/video/", $c));
350 | }
351 |
352 | if($n == 'twitter:image:src' || $p == 'og:image'){
353 | // Absolute url
354 | if(preg_match("/^(https?:)?\/\//", $c)) {
355 | $content["thumb"] = $c;
356 | }
357 |
358 | // Relative url from root
359 | elseif(preg_match("/^\//", $c)) {
360 | preg_match("/^((?:https?:)?\/\/([^\/]+))(\/|$)/", $l, $m);
361 | $content["thumb"] = $m[1].'/'.$c;
362 | }
363 |
364 | // Relative url from current directory
365 | else {
366 | preg_match("/^((?:https?:)?\/\/[^\/]+.*?)(\/[^\/]*)?$/", $l, $m);
367 | $content["thumb"] = $m[1].'/'.$c;
368 | }
369 | }
370 |
371 | if($n == 'twitter:domain'){
372 | $content["host"] = $c;
373 | }
374 | }
375 |
376 | return [
377 | "valid" => true,
378 | "content_type" => "link",
379 | "content" => $content
380 | ];
381 | }
382 |
383 | public static function upload_image(){
384 | self::login_protected();
385 |
386 | return Image::upload();
387 | }
388 |
389 | public static function load($r){
390 | $from = [];
391 | if(preg_match("/^[0-9]{4}-[0-9]{2}$/", @$r["filter"]["from"])){
392 | $from = $r["filter"]["from"]."-01 00:00";
393 | }
394 |
395 | if(preg_match("/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/", @$r["filter"]["from"])){
396 | $from = $r["filter"]["from"]." 00:00";
397 | }
398 |
399 | $to = [];
400 | if(preg_match("/^[0-9]{4}-[0-9]{2}$/", @$r["filter"]["to"])){
401 | $to = $r["filter"]["to"]."-01 00:00";
402 | }
403 |
404 | if(preg_match("/^[0-9]{4}-[0-9]{2}-[0-9]{2}$/", @$r["filter"]["to"])){
405 | $to = $r["filter"]["to"]." 00:00";
406 | }
407 |
408 | $id = [];
409 | if(@$r["filter"]["id"]){
410 | $id = intval($r["filter"]["id"]);
411 | }
412 |
413 | $tag = [];
414 | if(preg_match("/^[A-Za-z0-9-_]+$/", @$r["filter"]["tag"])){
415 | $tag = '#'.$r["filter"]["tag"];
416 | }
417 |
418 | $loc = [];
419 | if(@$r["filter"]["loc"]){
420 | $loc = $r["filter"]["loc"];
421 | }
422 |
423 | $person = [];
424 | if(@$r["filter"]["person"]){
425 | $person = $r["filter"]["person"];
426 | }
427 |
428 | if (DB::connection() === 'sqlite') {
429 | $datetime = "strftime('%d %m %Y %H:%M', `posts`.`datetime`)";
430 | } else if (DB::connection() === 'postgres') {
431 | $datetime = "to_char(posts.datetime,'DD Mon YYYY HH24:MI')";
432 | } else {
433 | $datetime = "DATE_FORMAT(`posts`.`datetime`,'%d %b %Y %H:%i')";
434 | }
435 |
436 | $like_match = "LIKE ".DB::concat("'%'", "?", "'%'");
437 |
438 | return DB::get_instance()->query("
439 | SELECT
440 | `id`, `text`, `feeling`, `persons`, `location`, `privacy`, `content_type`, `content`,
441 | $datetime AS `datetime`, (`status` <> 1) AS `is_hidden`
442 | FROM `posts`
443 | WHERE ".
444 | (!User::is_logged_in() ? (User::is_visitor() ? "`privacy` IN ('public', 'friends') AND " : "`privacy` = 'public' AND ") : "").
445 | ($from ? "`posts`.`datetime` > ? AND " : "").
446 | ($to ? "`posts`.`datetime` < ? AND " : "").
447 | ($id ? "`id` = ? AND " : "").
448 | ($tag ? "`plain_text` $like_match AND " : "").
449 | ($loc ? "`location` $like_match AND " : "").
450 | ($person ? "`persons` $like_match AND " : "").
451 | "`status` <> 5
452 | ORDER BY `posts`.`datetime` ".(@$r["sort"] == 'reverse' ? "ASC" : "DESC")."
453 | LIMIT ? OFFSET ?
454 | ", $from, $to, $id, $tag, $loc, $person, $r["limit"], $r["offset"]
455 | )->all();
456 | }
457 |
458 | public static function login($r){
459 | return User::login($r["nick"], $r["pass"]);
460 | }
461 |
462 | public static function logout(){
463 | return User::logout();
464 | }
465 |
466 | public static function handshake($r){
467 | return ["logged_in" => User::is_logged_in(), "is_visitor" => User::is_visitor()];
468 | }
469 | }
--------------------------------------------------------------------------------
/index.php:
--------------------------------------------------------------------------------
1 | %02d', $h, $h);
24 | }
25 |
26 | $minutes = '';
27 | for($m=0;$m<60;$m+=10){
28 | $minutes .= sprintf('', $m, $m);
29 | }
30 |
31 | $header_path = PROJECT_PATH.Config::get_safe("header", 'data/header.html');
32 | if(file_exists($header_path)){
33 | $header = file_get_contents($header_path);
34 | } else {
35 | $header = '';
36 | }
37 |
38 | // Translate styles into html
39 | $styles = Config::get_safe("styles", []);
40 | $styles_html = '';
41 | if(!empty($styles)){
42 | if(!is_array($styles)){
43 | $styles = [$styles];
44 | }
45 |
46 | $styles = array_unique($styles);
47 | $styles = array_map('escape', $styles);
48 | $styles_html = ''.PHP_EOL.''.PHP_EOL;
49 | }
50 |
51 | // Translate script urls into html
52 | $scripts = Config::get_safe("scripts", []);
53 | $scripts_html = '';
54 | if(!empty($scripts)){
55 | if(!is_array($scripts)){
56 | $scripts = [$scripts];
57 | }
58 |
59 | $scripts = array_unique($scripts);
60 | $scripts = array_map('escape', $scripts);
61 | $scripts_html = ''.PHP_EOL.''.PHP_EOL;
62 | }
63 |
64 | // Use version suffix in URLs to prevent cache
65 | $versionSuffix = '';
66 | if (Config::get_safe("version", false)) {
67 | $versionSuffix = '?v='.rawurlencode(Config::get("version"));
68 | }
69 |
70 | ?>
71 |
72 |
73 |
74 |
343 |
348 |
349 |
350 |
351 |
352 |
353 |
354 | '.PHP_EOL : ''; ?>
355 |
356 |
357 |
358 |
359 |
360 |
--------------------------------------------------------------------------------
/static/scripts/lightbox.js:
--------------------------------------------------------------------------------
1 | /*!
2 | * Lightbox v2.9.0
3 | * by Lokesh Dhakar
4 | *
5 | * More info:
6 | * http://lokeshdhakar.com/projects/lightbox2/
7 | *
8 | * Copyright 2007, 2015 Lokesh Dhakar
9 | * Released under the MIT license
10 | * https://github.com/lokesh/lightbox2/blob/master/LICENSE
11 | */
12 |
13 | // Uses Node, AMD or browser globals to create a module.
14 | (function (root, factory) {
15 | if (typeof define === 'function' && define.amd) {
16 | // AMD. Register as an anonymous module.
17 | define(['jquery'], factory);
18 | } else if (typeof exports === 'object') {
19 | // Node. Does not work with strict CommonJS, but
20 | // only CommonJS-like environments that support module.exports,
21 | // like Node.
22 | module.exports = factory(require('jquery'));
23 | } else {
24 | // Browser globals (root is window)
25 | root.lightbox = factory(root.jQuery);
26 | }
27 | }(this, function ($) {
28 |
29 | function Lightbox(options) {
30 | this.album = [];
31 | this.currentImageIndex = void 0;
32 | this.init();
33 |
34 | // options
35 | this.options = $.extend({}, this.constructor.defaults);
36 | this.option(options);
37 | }
38 |
39 | // Descriptions of all options available on the demo site:
40 | // http://lokeshdhakar.com/projects/lightbox2/index.html#options
41 | Lightbox.defaults = {
42 | albumLabel: 'Image %1 of %2',
43 | alwaysShowNavOnTouchDevices: false,
44 | fadeDuration: 600,
45 | fitImagesInViewport: true,
46 | imageFadeDuration: 600,
47 | // maxWidth: 800,
48 | // maxHeight: 600,
49 | positionFromTop: 50,
50 | resizeDuration: 700,
51 | showImageNumberLabel: true,
52 | wrapAround: false,
53 | disableScrolling: false,
54 | /*
55 | Sanitize Title
56 | If the caption data is trusted, for example you are hardcoding it in, then leave this to false.
57 | This will free you to add html tags, such as links, in the caption.
58 |
59 | If the caption data is user submitted or from some other untrusted source, then set this to true
60 | to prevent xss and other injection attacks.
61 | */
62 | sanitizeTitle: false
63 | };
64 |
65 | Lightbox.prototype.option = function(options) {
66 | $.extend(this.options, options);
67 | };
68 |
69 | Lightbox.prototype.imageCountLabel = function(currentImageNum, totalImages) {
70 | return this.options.albumLabel.replace(/%1/g, currentImageNum).replace(/%2/g, totalImages);
71 | };
72 |
73 | Lightbox.prototype.init = function() {
74 | var self = this;
75 | // Both enable and build methods require the body tag to be in the DOM.
76 | $(document).ready(function() {
77 | self.enable();
78 | self.build();
79 | });
80 | };
81 |
82 | // Loop through anchors and areamaps looking for either data-lightbox attributes or rel attributes
83 | // that contain 'lightbox'. When these are clicked, start lightbox.
84 | Lightbox.prototype.enable = function() {
85 | var self = this;
86 | $('body').on('click', 'a[rel^=lightbox], area[rel^=lightbox], a[data-lightbox], area[data-lightbox]', function(event) {
87 | self.start($(event.currentTarget));
88 | return false;
89 | });
90 | };
91 |
92 | // Build html for the lightbox and the overlay.
93 | // Attach event handlers to the new DOM elements. click click click
94 | Lightbox.prototype.build = function() {
95 | var self = this;
96 | $('').appendTo($('body'));
97 |
98 | // Cache jQuery objects
99 | this.$lightbox = $('#lightbox');
100 | this.$overlay = $('#lightboxOverlay');
101 | this.$outerContainer = this.$lightbox.find('.lb-outerContainer');
102 | this.$container = this.$lightbox.find('.lb-container');
103 | this.$image = this.$lightbox.find('.lb-image');
104 | this.$nav = this.$lightbox.find('.lb-nav');
105 |
106 | // Store css values for future lookup
107 | this.containerPadding = {
108 | top: parseInt(this.$container.css('padding-top'), 10),
109 | right: parseInt(this.$container.css('padding-right'), 10),
110 | bottom: parseInt(this.$container.css('padding-bottom'), 10),
111 | left: parseInt(this.$container.css('padding-left'), 10)
112 | };
113 |
114 | this.imageBorderWidth = {
115 | top: parseInt(this.$image.css('border-top-width'), 10),
116 | right: parseInt(this.$image.css('border-right-width'), 10),
117 | bottom: parseInt(this.$image.css('border-bottom-width'), 10),
118 | left: parseInt(this.$image.css('border-left-width'), 10)
119 | };
120 |
121 | // Attach event handlers to the newly minted DOM elements
122 | this.$overlay.hide().on('click', function() {
123 | self.end();
124 | return false;
125 | });
126 |
127 | this.$lightbox.hide().on('click', function(event) {
128 | if ($(event.target).attr('id') === 'lightbox') {
129 | self.end();
130 | }
131 | return false;
132 | });
133 |
134 | this.$outerContainer.on('click', function(event) {
135 | if ($(event.target).attr('id') === 'lightbox') {
136 | self.end();
137 | }
138 | return false;
139 | });
140 |
141 | this.$lightbox.find('.lb-prev').on('click', function() {
142 | if (self.currentImageIndex === 0) {
143 | self.changeImage(self.album.length - 1);
144 | } else {
145 | self.changeImage(self.currentImageIndex - 1);
146 | }
147 | return false;
148 | });
149 |
150 | this.$lightbox.find('.lb-next').on('click', function() {
151 | if (self.currentImageIndex === self.album.length - 1) {
152 | self.changeImage(0);
153 | } else {
154 | self.changeImage(self.currentImageIndex + 1);
155 | }
156 | return false;
157 | });
158 |
159 | /*
160 | Show context menu for image on right-click
161 |
162 | There is a div containing the navigation that spans the entire image and lives above of it. If
163 | you right-click, you are right clicking this div and not the image. This prevents users from
164 | saving the image or using other context menu actions with the image.
165 |
166 | To fix this, when we detect the right mouse button is pressed down, but not yet clicked, we
167 | set pointer-events to none on the nav div. This is so that the upcoming right-click event on
168 | the next mouseup will bubble down to the image. Once the right-click/contextmenu event occurs
169 | we set the pointer events back to auto for the nav div so it can capture hover and left-click
170 | events as usual.
171 | */
172 | this.$nav.on('mousedown', function(event) {
173 | if (event.which === 3) {
174 | self.$nav.css('pointer-events', 'none');
175 |
176 | self.$lightbox.one('contextmenu', function() {
177 | setTimeout(function() {
178 | this.$nav.css('pointer-events', 'auto');
179 | }.bind(self), 0);
180 | });
181 | }
182 | });
183 |
184 |
185 | this.$lightbox.find('.lb-loader, .lb-close').on('click', function() {
186 | self.end();
187 | return false;
188 | });
189 | };
190 |
191 | // Show overlay and lightbox. If the image is part of a set, add siblings to album array.
192 | Lightbox.prototype.start = function($link) {
193 | var self = this;
194 | var $window = $(window);
195 |
196 | $window.on('resize', $.proxy(this.sizeOverlay, this));
197 |
198 | $('select, object, embed').css({
199 | visibility: 'hidden'
200 | });
201 |
202 | this.sizeOverlay();
203 |
204 | this.album = [];
205 | var imageNumber = 0;
206 |
207 | function addToAlbum($link) {
208 | self.album.push({
209 | link: $link.attr('href'),
210 | title: $link.attr('data-title') || $link.attr('title')
211 | });
212 | }
213 |
214 | // Support both data-lightbox attribute and rel attribute implementations
215 | var dataLightboxValue = $link.attr('data-lightbox');
216 | var $links;
217 |
218 | if (dataLightboxValue) {
219 | $links = $($link.prop('tagName') + '[data-lightbox="' + dataLightboxValue + '"]');
220 | for (var i = 0; i < $links.length; i = ++i) {
221 | addToAlbum($($links[i]));
222 | if ($links[i] === $link[0]) {
223 | imageNumber = i;
224 | }
225 | }
226 | } else {
227 | if ($link.attr('rel') === 'lightbox') {
228 | // If image is not part of a set
229 | addToAlbum($link);
230 | } else {
231 | // If image is part of a set
232 | $links = $($link.prop('tagName') + '[rel="' + $link.attr('rel') + '"]');
233 | for (var j = 0; j < $links.length; j = ++j) {
234 | addToAlbum($($links[j]));
235 | if ($links[j] === $link[0]) {
236 | imageNumber = j;
237 | }
238 | }
239 | }
240 | }
241 |
242 | // Position Lightbox
243 | var top = $window.scrollTop() + this.options.positionFromTop;
244 | var left = $window.scrollLeft();
245 | this.$lightbox.css({
246 | top: top + 'px',
247 | left: left + 'px'
248 | }).fadeIn(this.options.fadeDuration);
249 |
250 | // Disable scrolling of the page while open
251 | if (this.options.disableScrolling) {
252 | $('body').addClass('lb-disable-scrolling');
253 | }
254 |
255 | this.changeImage(imageNumber);
256 | };
257 |
258 | // Hide most UI elements in preparation for the animated resizing of the lightbox.
259 | Lightbox.prototype.changeImage = function(imageNumber) {
260 | var self = this;
261 |
262 | this.disableKeyboardNav();
263 | var $image = this.$lightbox.find('.lb-image');
264 |
265 | this.$overlay.fadeIn(this.options.fadeDuration);
266 |
267 | $('.lb-loader').fadeIn('slow');
268 | this.$lightbox.find('.lb-image, .lb-nav, .lb-prev, .lb-next, .lb-dataContainer, .lb-numbers, .lb-caption').hide();
269 |
270 | this.$outerContainer.addClass('animating');
271 |
272 | // When image to show is preloaded, we send the width and height to sizeContainer()
273 | var preloader = new Image();
274 | preloader.onload = function() {
275 | var $preloader;
276 | var imageHeight;
277 | var imageWidth;
278 | var maxImageHeight;
279 | var maxImageWidth;
280 | var windowHeight;
281 | var windowWidth;
282 |
283 | $image.attr('src', self.album[imageNumber].link);
284 |
285 | $preloader = $(preloader);
286 |
287 | $image.width(preloader.width);
288 | $image.height(preloader.height);
289 |
290 | if (self.options.fitImagesInViewport) {
291 | // Fit image inside the viewport.
292 | // Take into account the border around the image and an additional 10px gutter on each side.
293 |
294 | windowWidth = $(window).width();
295 | windowHeight = $(window).height();
296 | maxImageWidth = windowWidth - self.containerPadding.left - self.containerPadding.right - self.imageBorderWidth.left - self.imageBorderWidth.right - 20;
297 | maxImageHeight = windowHeight - self.containerPadding.top - self.containerPadding.bottom - self.imageBorderWidth.top - self.imageBorderWidth.bottom - 120;
298 |
299 | // Check if image size is larger then maxWidth|maxHeight in settings
300 | if (self.options.maxWidth && self.options.maxWidth < maxImageWidth) {
301 | maxImageWidth = self.options.maxWidth;
302 | }
303 | if (self.options.maxHeight && self.options.maxHeight < maxImageWidth) {
304 | maxImageHeight = self.options.maxHeight;
305 | }
306 |
307 | // Is there a fitting issue?
308 | if ((preloader.width > maxImageWidth) || (preloader.height > maxImageHeight)) {
309 | if ((preloader.width / maxImageWidth) > (preloader.height / maxImageHeight)) {
310 | imageWidth = maxImageWidth;
311 | imageHeight = parseInt(preloader.height / (preloader.width / imageWidth), 10);
312 | $image.width(imageWidth);
313 | $image.height(imageHeight);
314 | } else {
315 | imageHeight = maxImageHeight;
316 | imageWidth = parseInt(preloader.width / (preloader.height / imageHeight), 10);
317 | $image.width(imageWidth);
318 | $image.height(imageHeight);
319 | }
320 | }
321 | }
322 | self.sizeContainer($image.width(), $image.height());
323 | };
324 |
325 | preloader.src = this.album[imageNumber].link;
326 | this.currentImageIndex = imageNumber;
327 | };
328 |
329 | // Stretch overlay to fit the viewport
330 | Lightbox.prototype.sizeOverlay = function() {
331 | this.$overlay
332 | .width($(document).width())
333 | .height($(document).height());
334 | };
335 |
336 | // Animate the size of the lightbox to fit the image we are showing
337 | Lightbox.prototype.sizeContainer = function(imageWidth, imageHeight) {
338 | var self = this;
339 |
340 | var oldWidth = this.$outerContainer.outerWidth();
341 | var oldHeight = this.$outerContainer.outerHeight();
342 | var newWidth = imageWidth + this.containerPadding.left + this.containerPadding.right + this.imageBorderWidth.left + this.imageBorderWidth.right;
343 | var newHeight = imageHeight + this.containerPadding.top + this.containerPadding.bottom + this.imageBorderWidth.top + this.imageBorderWidth.bottom;
344 |
345 | function postResize() {
346 | self.$lightbox.find('.lb-dataContainer').width(newWidth);
347 | self.$lightbox.find('.lb-prevLink').height(newHeight);
348 | self.$lightbox.find('.lb-nextLink').height(newHeight);
349 | self.showImage();
350 | }
351 |
352 | if (oldWidth !== newWidth || oldHeight !== newHeight) {
353 | this.$outerContainer.animate({
354 | width: newWidth,
355 | height: newHeight
356 | }, this.options.resizeDuration, 'swing', function() {
357 | postResize();
358 | });
359 | } else {
360 | postResize();
361 | }
362 | };
363 |
364 | // Display the image and its details and begin preload neighboring images.
365 | Lightbox.prototype.showImage = function() {
366 | this.$lightbox.find('.lb-loader').stop(true).hide();
367 | this.$lightbox.find('.lb-image').fadeIn(this.options.imageFadeDuration);
368 |
369 | this.updateNav();
370 | this.updateDetails();
371 | this.preloadNeighboringImages();
372 | this.enableKeyboardNav();
373 | };
374 |
375 | // Display previous and next navigation if appropriate.
376 | Lightbox.prototype.updateNav = function() {
377 | // Check to see if the browser supports touch events. If so, we take the conservative approach
378 | // and assume that mouse hover events are not supported and always show prev/next navigation
379 | // arrows in image sets.
380 | var alwaysShowNav = false;
381 | try {
382 | document.createEvent('TouchEvent');
383 | alwaysShowNav = (this.options.alwaysShowNavOnTouchDevices) ? true : false;
384 | } catch (e) {}
385 |
386 | this.$lightbox.find('.lb-nav').show();
387 |
388 | if (this.album.length > 1) {
389 | if (this.options.wrapAround) {
390 | if (alwaysShowNav) {
391 | this.$lightbox.find('.lb-prev, .lb-next').css('opacity', '1');
392 | }
393 | this.$lightbox.find('.lb-prev, .lb-next').show();
394 | } else {
395 | if (this.currentImageIndex > 0) {
396 | this.$lightbox.find('.lb-prev').show();
397 | if (alwaysShowNav) {
398 | this.$lightbox.find('.lb-prev').css('opacity', '1');
399 | }
400 | }
401 | if (this.currentImageIndex < this.album.length - 1) {
402 | this.$lightbox.find('.lb-next').show();
403 | if (alwaysShowNav) {
404 | this.$lightbox.find('.lb-next').css('opacity', '1');
405 | }
406 | }
407 | }
408 | }
409 | };
410 |
411 | // Display caption, image number, and closing button.
412 | Lightbox.prototype.updateDetails = function() {
413 | var self = this;
414 |
415 | // Enable anchor clicks in the injected caption html.
416 | // Thanks Nate Wright for the fix. @https://github.com/NateWr
417 | if (typeof this.album[this.currentImageIndex].title !== 'undefined' &&
418 | this.album[this.currentImageIndex].title !== '') {
419 | var $caption = this.$lightbox.find('.lb-caption');
420 | if (this.options.sanitizeTitle) {
421 | $caption.text(this.album[this.currentImageIndex].title);
422 | } else {
423 | $caption.html(this.album[this.currentImageIndex].title);
424 | }
425 | $caption.fadeIn('fast')
426 | .find('a').on('click', function(event) {
427 | if ($(this).attr('target') !== undefined) {
428 | window.open($(this).attr('href'), $(this).attr('target'));
429 | } else {
430 | location.href = $(this).attr('href');
431 | }
432 | });
433 | }
434 |
435 | if (this.album.length > 1 && this.options.showImageNumberLabel) {
436 | var labelText = this.imageCountLabel(this.currentImageIndex + 1, this.album.length);
437 | this.$lightbox.find('.lb-number').text(labelText).fadeIn('fast');
438 | } else {
439 | this.$lightbox.find('.lb-number').hide();
440 | }
441 |
442 | this.$outerContainer.removeClass('animating');
443 |
444 | this.$lightbox.find('.lb-dataContainer').fadeIn(this.options.resizeDuration, function() {
445 | return self.sizeOverlay();
446 | });
447 | };
448 |
449 | // Preload previous and next images in set.
450 | Lightbox.prototype.preloadNeighboringImages = function() {
451 | if (this.album.length > this.currentImageIndex + 1) {
452 | var preloadNext = new Image();
453 | preloadNext.src = this.album[this.currentImageIndex + 1].link;
454 | }
455 | if (this.currentImageIndex > 0) {
456 | var preloadPrev = new Image();
457 | preloadPrev.src = this.album[this.currentImageIndex - 1].link;
458 | }
459 | };
460 |
461 | Lightbox.prototype.enableKeyboardNav = function() {
462 | $(document).on('keyup.keyboard', $.proxy(this.keyboardAction, this));
463 | };
464 |
465 | Lightbox.prototype.disableKeyboardNav = function() {
466 | $(document).off('.keyboard');
467 | };
468 |
469 | Lightbox.prototype.keyboardAction = function(event) {
470 | var KEYCODE_ESC = 27;
471 | var KEYCODE_LEFTARROW = 37;
472 | var KEYCODE_RIGHTARROW = 39;
473 |
474 | var keycode = event.keyCode;
475 | var key = String.fromCharCode(keycode).toLowerCase();
476 | if (keycode === KEYCODE_ESC || key.match(/x|o|c/)) {
477 | this.end();
478 | } else if (key === 'p' || keycode === KEYCODE_LEFTARROW) {
479 | if (this.currentImageIndex !== 0) {
480 | this.changeImage(this.currentImageIndex - 1);
481 | } else if (this.options.wrapAround && this.album.length > 1) {
482 | this.changeImage(this.album.length - 1);
483 | }
484 | } else if (key === 'n' || keycode === KEYCODE_RIGHTARROW) {
485 | if (this.currentImageIndex !== this.album.length - 1) {
486 | this.changeImage(this.currentImageIndex + 1);
487 | } else if (this.options.wrapAround && this.album.length > 1) {
488 | this.changeImage(0);
489 | }
490 | }
491 | };
492 |
493 | // Closing time. :-(
494 | Lightbox.prototype.end = function() {
495 | this.disableKeyboardNav();
496 | $(window).off('resize', this.sizeOverlay);
497 | this.$lightbox.fadeOut(this.options.fadeDuration);
498 | this.$overlay.fadeOut(this.options.fadeDuration);
499 | $('select, object, embed').css({
500 | visibility: 'visible'
501 | });
502 | if (this.options.disableScrolling) {
503 | $('body').removeClass('lb-disable-scrolling');
504 | }
505 | };
506 |
507 | return new Lightbox();
508 | }));
509 |
--------------------------------------------------------------------------------
/static/styles/theme01.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: "Open Sans", Tahoma, sans-serif;
3 | color: #1d2129;
4 | background-color: #e9eaed;
5 | }
6 |
7 | .bluebar {
8 | background-color: #3b5998;
9 | border-bottom: 1px solid #29487d;
10 | color: #fff;
11 | height: 42px;
12 | line-height: 42px;
13 | position: relative;
14 | text-align: center;
15 | }
16 |
17 | .bluebar h1 {
18 | margin: 0;
19 | padding: 0;
20 | font-size: 24px;
21 | font-weight: 300;
22 | }
23 |
24 | .cover > img {
25 | max-width: 100%;
26 | max-height: 100%;
27 | }
28 |
29 | .headbar {
30 | max-width: 850px;
31 | margin: 0 auto;
32 | border: 1px solid #d3d6db;
33 | border-top: none;
34 | }
35 |
36 | .cover {
37 | font-size: 0;
38 | position: relative;
39 | /*min-height: 315px;*/
40 | }
41 | .cover .overlay {
42 | background: url(../images/UgNUNkKQar6.png) bottom left repeat-x;
43 | bottom: 0;
44 | left: 0;
45 | position: absolute;
46 | right: 0;
47 | top: 0;
48 | pointer-events: none;
49 | }
50 |
51 | .cover .profile {
52 | font-size: 0;
53 | position: absolute;
54 | left: 15px;
55 | bottom: -25px;
56 | width: 160px;
57 | height: 160px;
58 | border-radius: 3px;
59 | display: block;
60 | text-align: center;
61 | border: 1px solid rgba(0, 0, 0, .3);
62 | padding: 5px;
63 | border-radius: 2px;
64 | background-color: #fff;
65 | }
66 |
67 | .cover .profile img {
68 | max-width: 100%;
69 | max-height: 100%;
70 | }
71 |
72 | .cover .name {
73 | bottom: 12px;
74 | left: 201px;
75 | position: absolute;
76 | color: #fff;
77 | text-shadow: 0 0 3px rgba(0,0,0,.8);
78 | font-weight: bold;
79 | font-size: 24px;
80 | line-height: 30px;
81 | }
82 |
83 | @media only screen and (max-width: 502px) {
84 | .cover .profile {
85 | width: 80px;
86 | height: 80px;
87 | }
88 |
89 | .cover .name {
90 | left: 121px;
91 | font-size: 18px;
92 | line-height: 26px;
93 | }
94 | }
95 |
96 | #headline {
97 | min-height: 21px;
98 | padding: 10px;
99 | text-align: right;
100 | background-color: #fff;
101 | }
102 |
103 | #b_feed {
104 | margin: 0 auto;
105 | max-width: 502px;
106 | padding: 20px 0;
107 | }
108 |
109 | #eof_feed {
110 | text-align: center;
111 | margin-bottom: 20px;
112 | color: #90949c;
113 | font-size: 12px;
114 | text-transform: uppercase;
115 | }
116 |
117 | #eof_feed .link {
118 | color: #90949c;
119 | }
120 |
121 | .show_more {
122 | height: 40px;
123 | line-height: 40px;
124 | position: relative;
125 | margin-top: -40px;
126 | display: block;
127 | text-align: center;
128 | background: -moz-linear-gradient(top, rgba(255,255,255,0) 0%, rgba(255,255,255,1) 75%, rgba(255,255,255,1) 100%);
129 | background: -webkit-linear-gradient(top, rgba(255,255,255,0) 0%,rgba(255,255,255,1) 75%,rgba(255,255,255,1) 100%);
130 | background: linear-gradient(to bottom, rgba(255,255,255,0) 0%,rgba(255,255,255,1) 75%,rgba(255,255,255,1) 100%);
131 | filter: progid:DXImageTransform.Microsoft.gradient( startColorstr='#00ffffff', endColorstr='#ffffff',GradientType=0 );
132 | cursor: pointer;
133 | }
134 |
135 | .b_post {
136 | border: 1px solid;
137 | border-color: #e5e6e9 #dfe0e4 #d0d1d5;
138 | border-radius: 3px;
139 | background-color: #fff;
140 | margin-bottom: 10px;
141 | padding: 12px;
142 | position: relative;
143 | }
144 |
145 | .b_overlay {
146 | display: none;
147 | align-items: center;
148 | justify-content: center;
149 | position: absolute;
150 | top: 0;
151 | left: 0;
152 | width: 100%;
153 | height: 100%;
154 | z-index: 1;
155 | background: #ccc;
156 | }
157 |
158 | .b_overlay .button {
159 | background: white;
160 | }
161 |
162 | .b_post.is_hidden .b_overlay {
163 | display: flex;
164 | }
165 |
166 | .b_post.new_post {
167 | padding: 0;
168 | }
169 |
170 | .b_header {
171 | overflow: hidden;
172 | }
173 |
174 | .b_profile {
175 | float: left;
176 | }
177 |
178 | .b_desc {
179 | font-size: 14px;
180 | margin-right: 15px;
181 | margin-left: 45px;
182 | }
183 |
184 | .b_name {
185 | color: #365899;
186 | font-weight: bold;
187 | margin: 3px 0;
188 | }
189 |
190 | .b_options,
191 | .b_with,
192 | .b_here {
193 | color: #90949c;
194 | }
195 |
196 | .b_location {
197 | background: url(../images/theme01/tools.png) no-repeat;
198 | background-position: 2px 2px;
199 | padding-left: 15px;
200 | }
201 |
202 | .b_persons,
203 | .b_location {
204 | color: #365899;
205 | }
206 |
207 | .b_location:hover {
208 | cursor: pointer;
209 | text-decoration: underline;
210 | }
211 |
212 | .b_sharer {
213 | margin-bottom: 2px;
214 | }
215 |
216 | .b_date {
217 | font-size: 11px;
218 | color: #9197a3;
219 | text-decoration: none;
220 | }
221 |
222 | .b_date:hover {
223 | text-decoration: underline;
224 | }
225 |
226 | .b_tools {
227 | display: none;
228 | position: absolute;
229 | top: 5px;
230 | right: 5px;
231 | background: url(../images/theme01/Jid5DW8pIwZ.png) no-repeat;
232 | background-position: 5px 6px;
233 | height: 20px;
234 | width: 20px;
235 | }
236 |
237 | .b_tools:hover,
238 | .b_tools:active,
239 | .b_tools:focus {
240 | cursor: pointer;
241 | background: url(../images/theme01/B89i4luGsIu.png) no-repeat;
242 | background-position: 5px 6px;
243 | }
244 |
245 | .b_dropdown {
246 | display: none;
247 | text-align: left;
248 | background-color: #fff;
249 | border: 1px solid rgba(0, 0, 0, .15);
250 | border-radius: 3px;
251 | box-shadow: 0 3px 8px rgba(0, 0, 0, .3);
252 | z-index: 105;
253 | margin: 0;
254 | padding: 0;
255 | position: absolute;
256 | padding: 5px 0;
257 | }
258 |
259 | .b_dropdown li {
260 | list-style-type: none;
261 | }
262 |
263 | .b_dropdown li a {
264 | display: block;
265 | border: solid #fff;
266 | border-width: 1px 0;
267 | color: #1d2129;
268 | font-size: 13px;
269 | font-weight: normal;
270 | line-height: 22px;
271 | padding: 0 12px;
272 | }
273 |
274 | .b_dropdown li a:hover,
275 | .b_dropdown li a:active,
276 | .b_dropdown li a:focus {
277 | background-color: #4267b2;
278 | border-color: #29487d;
279 | color: #fff;
280 | cursor: pointer;
281 | }
282 |
283 | .b_text {
284 | word-wrap: break-word;
285 | font-size: 14px;
286 | padding-top: 10px;
287 | overflow: hidden;
288 | }
289 |
290 | .b_text a,
291 | .b_text .tag {
292 | color: #365899;
293 | text-decoration: none;
294 | }
295 |
296 | .b_text a:hover,
297 | .b_text a:active,
298 | .b_text a:focus {
299 | cursor: pointer;
300 | text-decoration: underline;
301 | }
302 |
303 | .b_link {
304 | padding: 1px;
305 | display: block;
306 | color: #1d2129;
307 | text-decoration: none;
308 | margin-top: 10px;
309 | box-shadow: 0 0 0 1px rgba(0, 0, 0, .15) inset, 0 1px 4px rgba(0, 0, 0, .1);
310 | overflow: hidden;
311 | }
312 |
313 | .b_link .thumb {
314 | width: 158px;
315 | height: 158px;
316 | float: left;
317 | position: relative;
318 | }
319 |
320 | .b_link .thumb img {
321 | width: 100%;
322 | height: 100%;
323 | }
324 |
325 | .b_link .thumb .play {
326 | background: url(../images/bNvHN6v1NeH.png) no-repeat;
327 | height: 54px;
328 | width: 54px;
329 | bottom: 0;
330 | left: 0;
331 | margin: auto;
332 | position: absolute;
333 | right: 0;
334 | top: 0;
335 | }
336 |
337 | .b_link .thumb:hover .play {
338 | background-position: 0 -55px;
339 | }
340 |
341 | .b_link .info {
342 | position: relative;
343 | padding: 10px 12px;
344 | height: 158px;
345 | box-sizing: border-box;
346 | }
347 |
348 | .b_link .info.has_thumb {
349 | margin-left: 158px;
350 | }
351 |
352 | .b_link .info .title {
353 | font-family: Georgia, serif;
354 | font-size: 18px;
355 | font-weight: 500;
356 | line-height: 22px;
357 | }
358 |
359 | .b_link .info .desc {
360 | margin-top: 5px;
361 | font-size: 13px;
362 | }
363 |
364 | .b_link .info .host {
365 | bottom: 10px;
366 | left: 12px;
367 | position: absolute;
368 | right: 12px;
369 | color: #90949c;
370 | font-size: 11px;
371 | line-height: 11px;
372 | text-transform: uppercase;
373 | background: white;
374 | }
375 |
376 | .b_imglink {
377 | display: block;
378 | color: #1d2129;
379 | text-decoration: none;
380 | margin-top: 10px;
381 | box-shadow: 0 1px 3px rgba(0, 0, 0, .41);
382 | overflow: hidden;
383 | position: relative;
384 | font-size: 0;
385 | text-align: center;
386 | }
387 |
388 | .b_imglink img {
389 | max-width: 100%;
390 | max-height: 470px;
391 | }
392 |
393 | .b_imglink .ftr {
394 | background: url(../images/QijIVO3ZIrO.png) repeat-x 0 0;
395 | bottom: 0;
396 | color: #fff;
397 | font-size: 11px;
398 | -webkit-font-smoothing: antialiased;
399 | font-weight: bold;
400 | height: 56px;
401 | left: 0;
402 | position: absolute;
403 | right: 0;
404 | text-align: left;
405 | text-shadow: 0 1px 4px rgba(0, 0, 0, .4);
406 | text-transform: uppercase;
407 | white-space: nowrap;
408 | }
409 |
410 | .b_imglink .ftr .host {
411 | bottom: 10px;
412 | left: 12px;
413 | position: absolute;
414 | right: 12px;
415 | font-size: 11px;
416 | line-height: 11px;
417 | text-transform: uppercase;
418 | }
419 |
420 | .b_imglink .ftr .desc {
421 | bottom: 25px;
422 | font-size: 14px;
423 | left: 11px;
424 | overflow: hidden;
425 | position: absolute;
426 | right: 44px;
427 | text-overflow: ellipsis;
428 | text-transform: none;
429 | }
430 |
431 | .b_imglink .ftr i.exit {
432 | background: url(../images/JNPO3NqYHEj.png) no-repeat;
433 | background-position: 0 -158px;
434 | width: 24px;
435 | height: 24px;
436 | display: inline-block;
437 | position: absolute;
438 | bottom: 9px;
439 | right: 10px;
440 | }
441 |
442 | .b_img {
443 | display: block;
444 | margin-top: 10px;
445 | overflow: hidden;
446 | position: relative;
447 | font-size: 0;
448 | text-align: center;
449 | }
450 |
451 | .b_img img {
452 | max-width: 100%;
453 | max-height: 470px;
454 | }
455 |
456 | .b_goal {
457 | text-align: center;
458 | font-size: 1.5em;
459 | margin: 5px 0;
460 | }
461 |
462 | .b_goal:before {
463 | content: " ";
464 | display: block;
465 | width: 40px;
466 | height: 40px;
467 | background: #3578e5 no-repeat center center;
468 | border-radius: 50%;
469 | line-height: 0;
470 | margin: 10px auto;
471 | }
472 |
473 | .b_goal.star:before {
474 | background-image: url(../images/star.png);
475 | }
476 |
477 | .b_goal.trophy:before {
478 | background-image: url(../images/trophy.png);
479 | }
480 |
481 | .b_textarea {
482 | width: 100%;
483 | max-width: 100%;
484 | min-width: 100%;
485 | height: 200px;
486 | min-height: 200px;
487 | border: 0;
488 | }
489 |
490 | .mask {
491 | display: none;
492 | position: fixed;
493 | top: 0;
494 | left: 0;
495 | bottom: 0;
496 | right: 0;
497 | width: 100%;
498 | height: 100%;
499 | z-index: 100;
500 | }
501 |
502 | .modal {
503 | overflow: auto;
504 | position: fixed;
505 | top: 0;
506 | right: 0;
507 | bottom: 0;
508 | left: 0;
509 | z-index: 95;
510 | outline: 0;
511 | background-color: rgba(0, 0, 0, .4);
512 | }
513 |
514 | .modal-dialog {
515 | max-width: 600px;
516 | margin: 30px auto;
517 | position: relative;
518 | width: auto;
519 | /*margin: 10px;*/
520 | }
521 |
522 | .modal-dialog.small {
523 | max-width: 400px;
524 | }
525 |
526 | .modal-content {
527 | position: relative;
528 | background-color: #fff;
529 | border: 1px solid #999;
530 | border: 1px solid rgba(0, 0, 0, 0.2);
531 | border-radius: 3px;
532 | -webkit-box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
533 | box-shadow: 0 5px 15px rgba(0, 0, 0, 0.5);
534 | outline: 0;
535 | }
536 |
537 | .modal-header {
538 | background-color: #f6f7f9;
539 | border-bottom: 1px solid #e5e5e5;
540 | color: #1d2129;
541 | font-weight: bold;
542 | line-height: 19px;
543 | padding: 10px 12px;
544 | }
545 |
546 | .modal-header:before,
547 | .modal-header:after {
548 | content: " ";
549 | display: table;
550 | }
551 |
552 | .modal-header:after {
553 | clear: both;
554 | }
555 |
556 | .modal-header .close {
557 | position: absolute;
558 | top: 15px;
559 | right: 15px;
560 | height: 12px;
561 | width: 12px;
562 | display: inline-block;
563 | background: url(../images/theme01/CAGlHC-HRGh.png) no-repeat;
564 | }
565 |
566 | .modal-header .close:hover,
567 | .modal-header .close:active,
568 | .modal-header .close:focus {
569 | background: url(../images/theme01/opUxrh_sBcu.png) no-repeat;
570 | cursor: pointer;
571 | }
572 |
573 | .modal-title {
574 | margin: 0;
575 | line-height: 1.42857;
576 | }
577 |
578 | .modal-body {
579 | position: relative;
580 | padding: 15px;
581 | }
582 |
583 | .modal-footer {
584 | height: 40px;
585 | text-align: right;
586 | border-top: 1px solid #dddfe2;
587 | }
588 |
589 | .modal-footer .buttons {
590 | margin: 8px;
591 | }
592 |
593 | .modal-footer:before,
594 | .modal-footer:after {
595 | content: " ";
596 | display: table;
597 | }
598 |
599 | .modal-footer:after {
600 | clear: both;
601 | }
602 |
603 | .button {
604 | display: inline-block;
605 | border: 1px solid;
606 | border-radius: 2px;
607 | box-sizing: content-box;
608 | font-size: 12px;
609 | line-height: 22px;
610 | font-weight: bold;
611 | padding: 0 16px;
612 | position: relative;
613 | text-align: center;
614 | text-shadow: none;
615 | vertical-align: middle;
616 | cursor: pointer;
617 | color: #000;
618 | }
619 |
620 | .button.gray {
621 | background-color: #f6f7f9;
622 | border-color: #ced0d4;
623 | color: #4b4f56;
624 | }
625 |
626 | .button.blue {
627 | color: #fff;
628 | background-color: #4267b2;
629 | border-color: #4267b2;
630 | }
631 |
632 | .login-form input {
633 | height: 44px;
634 | font-size: 16px;
635 | width: 100%;
636 | margin-bottom: 10px;
637 | -webkit-appearance: none;
638 | background: #fff;
639 | border: 1px solid #d9d9d9;
640 | border-top: 1px solid #c0c0c0;
641 | /* border-radius: 2px; */
642 | padding: 0 8px;
643 | box-sizing: border-box;
644 | -moz-box-sizing: border-box;
645 | }
646 |
647 | .login-form input:last-child {
648 | margin-bottom: 0;
649 | }
650 |
651 | .login-form input:hover {
652 | border: 1px solid #b9b9b9;
653 | border-top: 1px solid #a0a0a0;
654 | -moz-box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
655 | -webkit-box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
656 | box-shadow: inset 0 1px 2px rgba(0,0,0,0.1);
657 | }
658 |
659 | .e_profile {
660 | float: left;
661 | }
662 |
663 | .e_drag, .e_drop {
664 | display: none;
665 | position: absolute;
666 | top: 0;
667 | left: 0;
668 | width: 100%;
669 | height: 100%;
670 | background-color: rgba(255, 255, 255, .95);
671 | border: 2px dashed #7e97ba;
672 | border-radius: 3px;
673 | color: #7e97ba;
674 | font-size: 16px;
675 | font-weight: bold;
676 | text-align: center;
677 | box-sizing: border-box;
678 | pointer-events: none;
679 | }
680 |
681 | .e_drag span, .e_drop span {
682 | margin: auto;
683 | }
684 |
685 | .e_drop {
686 | border-color: #75a3f5;
687 | color: #75a3f5;
688 | }
689 |
690 | .e_loading {
691 | display: none;
692 | text-align: center;
693 | }
694 |
695 | .e_dots {
696 | width:8px;
697 | height:8px;
698 |
699 | animation-name: e_dots;
700 | animation-duration: 2s;
701 | animation-iteration-count: infinite;
702 | animation-timing-function: cubic-bezier(0.5, 0, 0.5, 1);
703 |
704 | background-color: black;
705 | border-radius: 4px;
706 | display: inline-block;
707 | }
708 |
709 | .e_dots:nth-child(2) {
710 | animation-delay:0.3s;
711 | }
712 |
713 | .e_dots:nth-child(3) {
714 | animation-delay:0.6s;
715 | }
716 |
717 | @keyframes e_dots {
718 | 0% {
719 | opacity: .265;
720 | transform: scale(.8,.8)
721 | }
722 |
723 | 5% {
724 | opacity: .25
725 | }
726 |
727 | 50% {
728 | transform: scale(1,1)
729 | }
730 |
731 | 55% {
732 | opacity: 1
733 | }
734 |
735 | 100% {
736 | opacity: .265;
737 | transform: scale(.8,.8)
738 | }
739 | }
740 |
741 | .e_loading .e_meter {
742 | height: 5px;
743 | position: relative;
744 | margin-top: 15px;
745 | box-shadow: inset 0 -1px 1px rgba(255,255,255,0.3);
746 | }
747 |
748 | .e_loading .e_meter > span {
749 | display: block;
750 | width: 0;
751 | height: 100%;
752 | background-color: #4267b2;
753 | box-shadow:
754 | inset 0 2px 9px rgba(255,255,255,0.3),
755 | inset 0 -2px 6px rgba(0,0,0,0.4);
756 | position: relative;
757 | overflow: hidden;
758 | }
759 |
760 | .e_text {
761 | margin-left: 50px;
762 | outline: 0;
763 | white-space: pre-line;
764 | min-height: 50px;
765 | }
766 |
767 | .t_area {
768 | overflow: hidden;
769 | padding: 0 16px 0 0;
770 | margin: 0;
771 | box-sizing: border-box;
772 | }
773 |
774 | .t_area .e_text {
775 | white-space: pre-wrap;
776 | margin: 0 0 0 10px;
777 | min-height: 88px;
778 | max-width: 100%;
779 | min-width: 100%;
780 | background: transparent;
781 | border: 0;
782 | }
783 |
784 | .options {
785 | text-align: left;
786 | float: left;
787 | margin: 0;
788 | padding: 0;
789 | font-size: 0;
790 | }
791 |
792 | .options li {
793 | list-style-type: none;
794 | display: inline-block;
795 | }
796 |
797 | .options li a {
798 | width: 40px;
799 | height: 40px;
800 | display: inline-block;
801 | background: url(../images/theme01/pkJbsArvXFu.png) no-repeat;
802 | background-color: #fff;
803 | cursor: pointer;
804 | border-right: 1px solid #e5e5e5;
805 | }
806 |
807 | .options li a:hover,
808 | .options li a:active,
809 | .options li a:focus {
810 | background-color: #f2f2f2;
811 | }
812 |
813 | .options li.kepet a {
814 | position: relative;
815 | }
816 |
817 | .options li.kepet a span {
818 | height: 100%;
819 | overflow: hidden;
820 | position: absolute;
821 | right: 0;
822 | top: 0;
823 | width: 100%;
824 | }
825 |
826 | .options li.kepet a span .photo_upload {
827 | bottom: 0;
828 | cursor: inherit;
829 | font-size: 1000px !important;
830 | height: 300px;
831 | margin: 0;
832 | opacity: 0;
833 | padding: 0;
834 | position: absolute;
835 | right: 0;
836 | }
837 |
838 | .options li a.active {
839 | background: url(../images/theme01/7W9WiMukPsP.png) no-repeat;
840 | }
841 |
842 | .options li.kepet a {
843 | background-position: 0 0;
844 | }
845 |
846 | .options li.feeling a {
847 | background-position: 0 -80px;
848 | }
849 |
850 | .options li.feeling a.active {
851 | background-position: 0 -160px;
852 | }
853 |
854 | .options li.persons a {
855 | background-position: 0 -120px;
856 | }
857 |
858 | .options li.persons a.active {
859 | background-position: 0 -200px;
860 | }
861 |
862 | .options li.location a {
863 | background-position: 0 -40px;
864 | }
865 |
866 | .options li.location a.active {
867 | background-position: 0 -120px;
868 | }
869 |
870 | .options_content {
871 | width: 100%;
872 | }
873 |
874 | .options_content tr {
875 | display: none;
876 | }
877 |
878 | .options_content th {
879 | width: 1%;
880 | white-space: nowrap;
881 | font-size: 13px;
882 | font-weight: normal;
883 | text-align: left;
884 | padding: 8px 6px 6px 8px;
885 | background-color: #e2e8f6;
886 | border: 1px solid #bdc7d8;
887 | }
888 |
889 | .options_content td {
890 | padding: 8px 6px 6px 8px;
891 | border: 1px solid #bdc7d8;
892 | position: relative;
893 | }
894 |
895 | .options_content td input {
896 | font-size: 13px;
897 | width: 100%;
898 | box-sizing: border-box;
899 | border: 0;
900 | padding: 0;
901 | margin: 0;
902 | outline: 0;
903 | }
904 |
905 | .options_content td .clear {
906 | position: absolute;
907 | top: 12px;
908 | right: 11px;
909 | }
910 |
911 | .clear {
912 | background: url(../images/theme01/W9Z74j1GbH2.png) no-repeat;
913 | display: inline-block;
914 | width: 10px;
915 | height: 10px;
916 | border: 0;
917 | padding: 0;
918 | margin: 0;
919 | }
920 |
921 | .clear:hover,
922 | .clear:active,
923 | .clear:focus {
924 | background: url(../images/theme01/wKDzFUeiPd3.png) no-repeat;
925 | }
926 |
927 | .content {
928 | display: none;
929 | padding-top: 0;
930 | }
931 |
932 | .content .clear {
933 | position: absolute;
934 | right: 7px;
935 | top: -7px;
936 | width: auto;
937 | height: auto;
938 | background: #6d6d6d;
939 | border-radius: 50%;
940 | cursor: pointer;
941 | font-size: 0 !important;
942 | overflow: hidden;
943 | vertical-align: middle;
944 | z-index: 100;
945 | }
946 |
947 | .content .clear:after {
948 | background: url(../images/theme01/y_KJ3X1mNCs.png) no-repeat;
949 | background-position: -446px -275px;
950 | display: inline-block;
951 | content: " ";
952 | height: 12px;
953 | width: 12px;
954 | margin: 3px;
955 | }
956 |
957 | .privacy {
958 | cursor: pointer;
959 | padding: 0 8px;
960 | }
961 |
962 | i.private,
963 | i.friends,
964 | i.public,
965 | i.arrow {
966 | background: url(../images/theme01/pkJbsArvXFu.png) no-repeat;
967 | display: inline-block;
968 | height: 16px;
969 | width: 16px;
970 | vertical-align: middle;
971 | margin-right: 3px;
972 | bottom: 1px;
973 | position: relative;
974 | }
975 |
976 | i.arrow {
977 | background: url(../images/theme01/y_KJ3X1mNCs.png) no-repeat;
978 | background-position: -147px -245px;
979 | width: 9px;
980 | height: 8px;
981 | margin-left: 4px;
982 | margin-right: 0;
983 | }
984 |
985 | i.private {
986 | background-position: -17px -211px;
987 | }
988 |
989 | .privacy_settings a:hover i.private {
990 | background-position: 0 -211px;
991 | }
992 |
993 | i.friends {
994 | background-position: -17px -177px;
995 | }
996 |
997 | .privacy_settings a:hover i.friends {
998 | background-position: 0 -177px;
999 | }
1000 |
1001 | i.public {
1002 | background-position: 0 -270px;
1003 | }
1004 |
1005 | .privacy_settings a:hover i.public {
1006 | background: url(../images/theme01/y_KJ3X1mNCs.png) no-repeat;
1007 | background-position: -68px -275px;
1008 | }
1009 |
1010 | .error {
1011 | color: #a94442;
1012 | background-color: #f2dede;
1013 | padding: 15px;
1014 | margin-bottom: 20px;
1015 | border: 1px solid #ebccd1;
1016 | border-radius: 4px;
1017 | }
1018 |
1019 | .error .clear {
1020 | float: right;
1021 | margin-top: 5px;
1022 | margin-left: 15px;
1023 | }
1024 |
1025 | body > .error {
1026 | position: fixed;
1027 | left: 10px;
1028 | bottom: 10px;
1029 | max-width: 300px;
1030 | }
1031 |
1032 | .more_posts {
1033 | display: none;
1034 | text-align: center;
1035 | margin-bottom: 10px;
1036 | }
1037 |
1038 | .tag {
1039 | cursor: pointer;
1040 | }
1041 |
1042 | .tag:hover,
1043 | .tag:active {
1044 | text-decoration: underline;
1045 | }
1046 |
1047 | code {
1048 | overflow: auto;
1049 | white-space: pre;
1050 | margin: 5px 0;
1051 | }
1052 |
1053 | /* datepicker */
1054 | .datepicker {
1055 | text-align: center;
1056 | }
1057 | .datepicker table {
1058 | width: 100%;
1059 | margin: 5px 0;
1060 | }
1061 |
1062 | .datepicker th,
1063 | .datepicker td {
1064 | width: 12.5%;
1065 | }
1066 |
1067 | .datepicker th {
1068 | padding: 5px 0;
1069 | }
1070 | .datepicker td {
1071 | color: #999;
1072 | padding: 5px;
1073 | border: 1px solid transparent;
1074 | }
1075 | .datepicker td.active {
1076 | color: black;
1077 | }
1078 |
1079 | .datepicker td.selected {
1080 | color: black;
1081 | background-color: #4267b2;
1082 | }
1083 |
1084 | .datepicker td.today {
1085 | position: relative;
1086 | }
1087 |
1088 | .datepicker td.today:after {
1089 | width: 10px;
1090 | height: 10px;
1091 | top: 50%;
1092 | margin-top: -5px;
1093 | right: 5px;
1094 | position: absolute;
1095 | display: inline-block;
1096 | content: ' ';
1097 | background-color: red;
1098 | border-radius: 100%;
1099 | }
1100 |
1101 | .datepicker td:hover {
1102 | background-color: #ced0d4;
1103 | border-color: #ced0d4;
1104 | cursor: pointer;
1105 | }
1106 |
1107 | .datepicker td.selected {
1108 | color: white;
1109 | border-color: #4267b2;
1110 | background-color: #4267b2;
1111 | }
1112 |
--------------------------------------------------------------------------------
344 | © 2016-2022 | ||||||