401
29 |unauthorized
30 |You tried to access a protected resource without providing proper authentication credentials.
31 | 32 |├── .gitignore ├── composer.json ├── composer.lock ├── docker-compose.yml.example ├── docker ├── nginx │ ├── Dockerfile │ └── aprelendo.conf └── php │ ├── Dockerfile │ ├── error-reporting.ini │ ├── uploads.ini │ └── xdebug.ini ├── license.md ├── readme.md ├── src ├── Includes │ ├── Classes │ │ ├── AIBot.php │ │ ├── Achievements.php │ │ ├── ArchivedTexts.php │ │ ├── AudioFile.php │ │ ├── AudioPlayer.php │ │ ├── AudioPlayerForEbooks.php │ │ ├── AudioPlayerForTexts.php │ │ ├── Card.php │ │ ├── Connect.php │ │ ├── Conversion.php │ │ ├── Curl.php │ │ ├── DBEntity.php │ │ ├── Dictionaries.php │ │ ├── EbookFile.php │ │ ├── EmailSender.php │ │ ├── ExampleSentences.php │ │ ├── File.php │ │ ├── Gems.php │ │ ├── InternalException.php │ │ ├── Language.php │ │ ├── Likes.php │ │ ├── Log.php │ │ ├── LogAudioStreams.php │ │ ├── LogFileUploads.php │ │ ├── Pagination.php │ │ ├── PopularSources.php │ │ ├── Preferences.php │ │ ├── RSSFeed.php │ │ ├── Reader.php │ │ ├── ReportedTexts.php │ │ ├── SM2.php │ │ ├── SearchParameters.php │ │ ├── SearchTextsParameters.php │ │ ├── SearchWordsParameters.php │ │ ├── SecureEncryption.php │ │ ├── SharedTextTable.php │ │ ├── SharedTexts.php │ │ ├── Table.php │ │ ├── TextTable.php │ │ ├── Texts.php │ │ ├── TextsUtilities.php │ │ ├── Token.php │ │ ├── Url.php │ │ ├── User.php │ │ ├── UserAuth.php │ │ ├── UserException.php │ │ ├── UserPassword.php │ │ ├── UserRegistrationManager.php │ │ ├── Videos.php │ │ ├── VoiceRSS.php │ │ ├── WordDailyGoal.php │ │ ├── WordFrequency.php │ │ ├── WordStats.php │ │ ├── WordTable.php │ │ ├── Words.php │ │ └── WordsUtilities.php │ ├── checklogin.php │ └── dbinit.php ├── config │ ├── aprelendo-schema.sql │ └── config-example.php └── public │ ├── 401.php │ ├── 403.php │ ├── 404.php │ ├── 500.php │ ├── aboutus.php │ ├── accountactivation.php │ ├── achievementsmodal.php │ ├── activitymonitor.php │ ├── addebook.php │ ├── addrss.php │ ├── addtext.php │ ├── addvideo.php │ ├── ajax │ ├── addtext.php │ ├── addword.php │ ├── archivetext.php │ ├── contact.php │ ├── deleteaccount.php │ ├── ebookposition.php │ ├── editlanguage.php │ ├── exportwords.php │ ├── fetchaudiostream.php │ ├── fetchrssfeeds.php │ ├── fetchurl.php │ ├── fetchvideo.php │ ├── forgotpassword.php │ ├── getaireply.php │ ├── getcards.php │ ├── getdicuris.php │ ├── getebook.php │ ├── getebookaudio.php │ ├── getstats.php │ ├── getuserwords.php │ ├── getwordfreq.php │ ├── google_oauth.php │ ├── login.php │ ├── register.php │ ├── removetext.php │ ├── removeword.php │ ├── reporttext.php │ ├── savepreferences.php │ ├── saveuserprofile.php │ ├── togglelike.php │ ├── updatecard.php │ └── updateuserscore.php │ ├── attributions.php │ ├── contact.php │ ├── css │ ├── 401.css │ ├── 401.min.css │ ├── 403.css │ ├── 403.min.css │ ├── 404.css │ ├── 404.min.css │ ├── 500.css │ ├── 500.min.css │ ├── ebooks.css │ ├── ebooks.min.css │ ├── styles.css │ └── styles.min.css │ ├── donate.php │ ├── editlanguage.php │ ├── eucookies.php │ ├── exampledics.php │ ├── extensions.php │ ├── footer.php │ ├── forgotpassword.php │ ├── head.php │ ├── header.php │ ├── img │ ├── avatar-pablo.jpg │ ├── backgrounds │ │ ├── pattern-wallpaper.png │ │ └── welcome-page-background.jpg │ ├── email-logo.png │ ├── favicons │ │ ├── apple-touch-icon.png │ │ ├── favicon-96x96.png │ │ ├── favicon.ico │ │ ├── favicon.svg │ │ ├── site.webmanifest │ │ ├── web-app-manifest-192x192.png │ │ └── web-app-manifest-512x512.png │ ├── flags │ │ ├── ar.svg │ │ ├── bg.svg │ │ ├── ca.svg │ │ ├── cs.svg │ │ ├── da.svg │ │ ├── de.svg │ │ ├── el.svg │ │ ├── en.svg │ │ ├── es.svg │ │ ├── fr.svg │ │ ├── he.svg │ │ ├── hi.svg │ │ ├── hr.svg │ │ ├── hu.svg │ │ ├── it.svg │ │ ├── ja.svg │ │ ├── ko.svg │ │ ├── nl.svg │ │ ├── no.svg │ │ ├── pl.svg │ │ ├── pt.svg │ │ ├── ro.svg │ │ ├── ru.svg │ │ ├── sk.svg │ │ ├── sl.svg │ │ ├── sv.svg │ │ ├── tr.svg │ │ ├── un.svg │ │ ├── vi.svg │ │ └── zh.svg │ ├── gamification │ │ ├── achievements │ │ │ ├── gems-100.png │ │ │ ├── gems-100k.png │ │ │ ├── gems-10k.png │ │ │ ├── gems-1k.png │ │ │ ├── gems-1m.png │ │ │ ├── gems-200.png │ │ │ ├── gems-200k.png │ │ │ ├── gems-25k.png │ │ │ ├── gems-3k.png │ │ │ ├── gems-500.png │ │ │ ├── gems-500k.png │ │ │ ├── gems-50k.png │ │ │ ├── gems-5k.png │ │ │ ├── streak-1095.png │ │ │ ├── streak-14.png │ │ │ ├── streak-180.png │ │ │ ├── streak-1825.png │ │ │ ├── streak-2.png │ │ │ ├── streak-30.png │ │ │ ├── streak-365.png │ │ │ ├── streak-7.png │ │ │ ├── streak-730.png │ │ │ ├── streak-90.png │ │ │ ├── words-100.png │ │ │ ├── words-10k.png │ │ │ ├── words-12k.png │ │ │ ├── words-15k.png │ │ │ ├── words-17k.png │ │ │ ├── words-1k.png │ │ │ ├── words-20k.png │ │ │ ├── words-25k.png │ │ │ ├── words-2k.png │ │ │ ├── words-3k.png │ │ │ ├── words-4k.png │ │ │ ├── words-500.png │ │ │ ├── words-5k.png │ │ │ └── words-7k.png │ │ ├── daily-goal-streak.png │ │ ├── gems.png │ │ ├── streak.png │ │ └── words-today.png │ ├── logo-long.png │ ├── logo.png │ └── other │ │ ├── cookie-dude.gif │ │ ├── immersion-banner.jpg │ │ └── reading-banner.jpg │ ├── index.php │ ├── js │ ├── achievementsmodal.js │ ├── achievementsmodal.min.js │ ├── actionbtns.js │ ├── actionbtns.min.js │ ├── addebook.js │ ├── addebook.min.js │ ├── addrss.js │ ├── addrss.min.js │ ├── addtext.js │ ├── addtext.min.js │ ├── addvideo.js │ ├── addvideo.min.js │ ├── audioplayer.js │ ├── audioplayer.min.js │ ├── contact.js │ ├── contact.min.js │ ├── cookies.js │ ├── cookies.min.js │ ├── dictation.js │ ├── dictation.min.js │ ├── dictionaries.js │ ├── dictionaries.min.js │ ├── epubjs │ │ └── epub.min.js │ ├── eucookies.js │ ├── eucookies.min.js │ ├── forgotpassword.js │ ├── forgotpassword.min.js │ ├── helpers.js │ ├── helpers.min.js │ ├── languages.js │ ├── languages.min.js │ ├── likes.js │ ├── likes.min.js │ ├── listtexts.js │ ├── listtexts.min.js │ ├── listwords.js │ ├── listwords.min.js │ ├── login.js │ ├── login.min.js │ ├── matomo.js │ ├── matomo.min.js │ ├── password.js │ ├── password.min.js │ ├── preferences.js │ ├── preferences.min.js │ ├── register.js │ ├── register.min.js │ ├── showaibotmodal.js │ ├── showaibotmodal.min.js │ ├── showebook.js │ ├── showebook.min.js │ ├── showimportwordsmodal.js │ ├── showimportwordsmodal.min.js │ ├── showofflinevideo.js │ ├── showofflinevideo.min.js │ ├── showreadersettingsmodal.js │ ├── showreadersettingsmodal.min.js │ ├── showreporttextmodal.js │ ├── showreporttextmodal.min.js │ ├── showtext.js │ ├── showtext.min.js │ ├── showvideo.js │ ├── showvideo.min.js │ ├── stats.js │ ├── stats.min.js │ ├── study.js │ ├── study.min.js │ ├── subtitles-parser │ │ ├── subtitles.parser.js │ │ └── subtitles.parser.min.js │ ├── tooltips.js │ ├── tooltips.min.js │ ├── underlinewords.js │ ├── underlinewords.min.js │ ├── userprofile.js │ ├── userprofile.min.js │ ├── videoplayer.js │ ├── videoplayer.min.js │ ├── wordselection.js │ ├── wordselection.min.js │ ├── ytvideoplayer.js │ └── ytvideoplayer.min.js │ ├── languages.php │ ├── listlanguages.php │ ├── listsharedtexts.php │ ├── listsources.php │ ├── listtexts.php │ ├── listwords.php │ ├── login.php │ ├── logout.php │ ├── preferences.php │ ├── privacy.php │ ├── register.php │ ├── sharedtexts.php │ ├── showaibotmodal.php │ ├── showdicactionmenu.php │ ├── showebook.php │ ├── showimportwordsmodal.php │ ├── showofflinevideo.php │ ├── showreadersettingsmodal.php │ ├── showreporttextmodal.php │ ├── showtext.php │ ├── showvideo.php │ ├── simpleheader.php │ ├── sources.php │ ├── stats.php │ ├── study.php │ ├── termsofservice.php │ ├── texts.php │ ├── textstats.php │ ├── totalreading.php │ ├── userprofile.php │ └── words.php └── templates ├── password-reset.html └── welcome.html /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | vendor/ 3 | src/config/config.php 4 | src/public/tests/ 5 | uploads/* 6 | todo.md 7 | .rsyncignore 8 | **/*.log 9 | docker-compose.yml -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "require": { 3 | "phpmailer/phpmailer": "^6.9" 4 | }, 5 | "autoload": { 6 | "psr-4": { 7 | "Aprelendo\\":"src/Includes/Classes/" 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docker-compose.yml.example: -------------------------------------------------------------------------------- 1 | services: 2 | nginx: 3 | container_name: aprelendo_nginx 4 | build: 5 | context: . 6 | dockerfile: docker/nginx/Dockerfile 7 | ports: 8 | - 80:80 9 | - 443:443 10 | volumes: 11 | - ./docker/nginx/aprelendo.conf:/etc/nginx/conf.d/aprelendo.conf 12 | - .:/var/www/aprelendo 13 | php: 14 | container_name: aprelendo_php 15 | build: 16 | context: . 17 | dockerfile: docker/php/Dockerfile 18 | extra_hosts: 19 | - "host.docker.internal:host-gateway" 20 | volumes: 21 | #- .:/var/www/aprelendo ## only use this if on development env 22 | - ./docker/php/uploads.ini:/usr/local/etc/php/conf.d/uploads.ini 23 | - ./docker/php/xdebug.ini:/usr/local/etc/php/conf.d/xdebug.ini 24 | - ./docker/php/error-reporting.ini:/usr/local/etc/php/conf.d/error-reporting.ini 25 | mysql: 26 | image: mariadb:latest 27 | container_name: aprelendo_db 28 | environment: 29 | MYSQL_ROOT_PASSWORD: 'root_password' ## change as necessary 30 | MYSQL_USER: 'aprelendo_user' ## change and keep the same as in src/config/config.php 31 | MYSQL_PASSWORD: 'aprelendo_user_password' ## change and keep the same as in src/config/config.php 32 | MYSQL_DATABASE: 'aprelendo' 33 | volumes: 34 | - mysqldata:/var/lib/mysql 35 | ports: 36 | - 3306:3306 37 | phpmyadmin: 38 | image: phpmyadmin/phpmyadmin 39 | container_name: phpmyadmin 40 | restart: always 41 | links: 42 | - mysql 43 | environment: 44 | PMA_HOST: mysql 45 | PMA_PORT: 3306 46 | MYSQL_ROOT_PASSWORD: 'root_password' ## change as necessary 47 | UPLOAD_LIMIT: 64M 48 | ports: 49 | - 8081:80 50 | volumes: 51 | mysqldata: {} 52 | -------------------------------------------------------------------------------- /docker/nginx/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM nginx 2 | 3 | RUN openssl req -x509 -nodes -days 365 \ 4 | -subj "/C=CA/ST=ON/O=Aprelendo/CN=aprelendo.com" \ 5 | -newkey rsa:2048 -keyout /etc/nginx/cert.key \ 6 | -out /etc/nginx/cert.pem; 7 | -------------------------------------------------------------------------------- /docker/nginx/aprelendo.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 443 ssl http2; 3 | listen [::]:443 ssl http2; 4 | 5 | #listen 80 default_server; 6 | 7 | client_max_body_size 20M; 8 | 9 | ## SSL self-signed certificate 10 | ssl_certificate /etc/nginx/cert.pem; 11 | ssl_certificate_key /etc/nginx/cert.key; 12 | 13 | # Add headers to serve security related headers 14 | # Before enabling Strict-Transport-Security headers please read into this 15 | # topic first. 16 | #add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"; 17 | #add_header X-Content-Type-Options nosniff; 18 | #add_header X-XSS-Protection "1; mode=block"; 19 | #add_header X-Robots-Tag none; 20 | #add_header X-Download-Options noopen; 21 | #add_header X-Permitted-Cross-Domain-Policies none; 22 | #add_header Referrer-Policy "no-referrer"; 23 | 24 | charset UTF-8; 25 | charset_types text/xml text/plain text/vnd.wap.wml application/x-javascript image/svg+xml application/rss+xml text/css application/javascript application/json; 26 | 27 | root /var/www/aprelendo/src/public; 28 | 29 | index index.php index.html index.htm; 30 | 31 | error_page 401 /401; 32 | error_page 403 /403; 33 | error_page 404 /404; 34 | error_page 500 502 503 504 /500; 35 | 36 | location ~ \.php$ { 37 | fastcgi_pass php:9000; 38 | fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name; 39 | include fastcgi_params; 40 | fastcgi_intercept_errors on; 41 | } 42 | 43 | location / { 44 | try_files $uri $uri.html $uri/ @extensionless-php; 45 | index index.php; 46 | } 47 | 48 | location @extensionless-php { 49 | rewrite ^(.*)$ $1.php last; 50 | } 51 | } 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /docker/php/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:fpm-alpine 2 | 3 | RUN apk add --no-cache autoconf gcc g++ linux-headers make python3 py3-pip python3-dev shadow 4 | 5 | # Set working directory 6 | WORKDIR /opt/venv 7 | 8 | # Create and activate virtual environment 9 | ENV VIRT_ENV=/opt/venv 10 | RUN python3 -m venv "$VIRT_ENV" 11 | ENV PATH="$VIRT_ENV/bin:$PATH" 12 | 13 | # Upgrade pip and setuptools within the virtual environment 14 | RUN source bin/activate && pip install --upgrade pip setuptools && deactivate 15 | 16 | # Install youtube_transcript_api within the virtual environment 17 | RUN source bin/activate && pip install youtube_transcript_api && deactivate 18 | 19 | # Install PHP extensions 20 | RUN docker-php-ext-install pdo pdo_mysql 21 | RUN pecl install xdebug && docker-php-ext-enable xdebug 22 | 23 | # Install Composer 24 | COPY --from=composer:latest /usr/bin/composer /usr/local/bin/composer 25 | 26 | # Set the working directory 27 | WORKDIR /var/www/aprelendo/ 28 | 29 | # Copy the composer.json and composer.lock files to the working directory 30 | COPY composer.json composer.lock /var/www/aprelendo/ 31 | 32 | # Install the PHP dependencies 33 | RUN composer install --no-scripts --no-autoloader 34 | 35 | # Make sure www-data group and user have ID 33, same as in Ubuntu. 36 | RUN groupmod -g 33 www-data && \ 37 | usermod -u 33 -g 33 www-data 38 | 39 | # Copy the rest of the application code 40 | COPY --chown=www-data:www-data . /var/www/aprelendo/ 41 | 42 | # Ensure correct permissions again after copy 43 | RUN chown -R www-data:www-data /var/www/aprelendo/ 44 | 45 | # Run the autoloader 46 | RUN composer dump-autoload --optimize 47 | -------------------------------------------------------------------------------- /docker/php/error-reporting.ini: -------------------------------------------------------------------------------- 1 | error_reporting=E_ALL 2 | -------------------------------------------------------------------------------- /docker/php/uploads.ini: -------------------------------------------------------------------------------- 1 | file_uploads = On 2 | memory_limit = 64M 3 | upload_max_filesize = 64M 4 | post_max_size = 64M 5 | max_execution_time = 600 6 | -------------------------------------------------------------------------------- /docker/php/xdebug.ini: -------------------------------------------------------------------------------- 1 | zend_extension=xdebug 2 | 3 | [xdebug] 4 | xdebug.mode=debug 5 | xdebug.client_host=host.docker.internal 6 | -------------------------------------------------------------------------------- /src/Includes/Classes/ArchivedTexts.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | namespace Aprelendo; 22 | 23 | class ArchivedTexts extends Texts 24 | { 25 | /** 26 | * Constructor 27 | * 28 | * @param \PDO $pdo 29 | * @param int $user_id 30 | * @param int $lang_id 31 | */ 32 | public function __construct(\PDO $pdo, int $user_id, int $lang_id) 33 | { 34 | parent::__construct($pdo, $user_id, $lang_id); 35 | $this->table = 'archived_texts'; 36 | } // end __construct() 37 | 38 | /** 39 | * Unarchives text by using their $ids 40 | * 41 | * @param array $text_ids 42 | * @return void 43 | */ 44 | public function unarchive(array $text_ids): void 45 | { 46 | $id_params = str_repeat("?,", count($text_ids)-1) . "?"; 47 | 48 | $sql = "INSERT INTO `texts` SELECT * FROM `{$this->table}` WHERE `id` IN ($id_params)"; 49 | $this->sqlExecute($sql, $text_ids); 50 | 51 | $sql = "DELETE FROM `{$this->table}` WHERE `id` IN ($id_params)"; 52 | $this->sqlExecute($sql, $text_ids); 53 | } 54 | } // end unarchive() 55 | -------------------------------------------------------------------------------- /src/Includes/Classes/AudioFile.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | namespace Aprelendo; 22 | 23 | class AudioFile extends File 24 | { 25 | /** 26 | * Constructor 27 | * @param string $file_name 28 | */ 29 | public function __construct(string $file_name) 30 | { 31 | parent::__construct($file_name); 32 | $this->allowed_extensions = ['mp3', 'ogg']; 33 | $this->max_size = 2097152; // 2 MB 34 | } // end __construct() 35 | } 36 | -------------------------------------------------------------------------------- /src/Includes/Classes/AudioPlayer.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | namespace Aprelendo; 22 | 23 | class AudioPlayer 24 | { 25 | protected string $audio_uri; 26 | 27 | /** 28 | * Constructor 29 | * Calls createFullReader or createMiniReader depending on the nr. of args 30 | */ 31 | public function __construct(string $audio_url) 32 | { 33 | $this->audio_uri = $audio_url; 34 | } // end __construct() 35 | 36 | /** 37 | * Return audio MIME type based on URI file extension 38 | * 39 | * @return string 40 | */ 41 | protected function getAudioMimeType(): string { 42 | // Get file extension 43 | $file_extension = pathinfo($this->audio_uri, PATHINFO_EXTENSION); 44 | 45 | // Map file extensions to audio types 46 | $audio_types = array( 47 | 'mp3' => 'audio/mpeg', 48 | 'ogg' => 'audio/ogg', 49 | 'wav' => 'audio/wav', 50 | 'aac' => 'audio/aac', 51 | 'm4a' => 'audio/x-m4a', 52 | 'webm' => 'audio/webm', 53 | 'flac' => 'audio/flac', 54 | 'opus' => 'audio/opus', 55 | ); 56 | 57 | // Set the appropriate MIME type based on the file extension 58 | return isset($audio_types[$file_extension]) ? $audio_types[$file_extension] : 'audio/mpeg'; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Includes/Classes/Conversion.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | namespace Aprelendo; 22 | 23 | class Conversion 24 | { 25 | /** 26 | * Converts JSON to CSV 27 | * 28 | * @param string $json in JSON format 29 | * @return string in CSV format 30 | */ 31 | public static function jsonToCsv(string $json) 32 | { 33 | return implode(',', json_decode($json)); 34 | } // end jsonToCsv() 35 | 36 | /** 37 | * Converts Array to CSV 38 | * 39 | * @param array 40 | * @return string in CSV format 41 | */ 42 | public static function arrayToCsv(array $array): string 43 | { 44 | if (is_array($array)) { 45 | return "'" . implode("','", $array) . "'"; 46 | } else { 47 | return "'$array'"; 48 | } 49 | } // end arrayToCsv() 50 | 51 | /** 52 | * Convert an array to XML 53 | * @param array $array 54 | * @param SimpleXMLElement $xml 55 | */ 56 | public static function arrayToXml($array, &$xml) 57 | { 58 | foreach ($array as $key => $value) { 59 | if (is_int($key)) { 60 | $key = "e"; 61 | } 62 | if (is_array($value)) { 63 | $label = $xml->addChild($key); 64 | self::arrayToXml($value, $label); 65 | } else { 66 | $xml->addChild($key, $value); 67 | } 68 | } 69 | } 70 | 71 | } 72 | -------------------------------------------------------------------------------- /src/Includes/Classes/EbookFile.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | namespace Aprelendo; 22 | 23 | class EbookFile extends File 24 | { 25 | /** 26 | * Constructor 27 | * @param string $file_name 28 | */ 29 | public function __construct(string $file_name) 30 | { 31 | parent::__construct($file_name); 32 | $this->allowed_extensions = ['epub']; 33 | $this->max_size = 2097152; // 2 MB 34 | } // end __construct() 35 | } 36 | -------------------------------------------------------------------------------- /src/Includes/Classes/EmailSender.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | namespace Aprelendo; 22 | 23 | use PHPMailer\PHPMailer\PHPMailer; 24 | use PHPMailer\PHPMailer\SMTP; 25 | 26 | class EmailSender 27 | { 28 | public $mail; 29 | 30 | /** 31 | * Constructor 32 | */ 33 | public function __construct() 34 | { 35 | $this->mail = new PHPMailer(true); // passing true enables exceptions 36 | $this->mail->CharSet = 'UTF-8'; 37 | $this->mail->SMTPDebug = SMTP::DEBUG_OFF; 38 | $this->mail->isSMTP(); 39 | $this->mail->Host = EMAIL_HOST; 40 | $this->mail->SMTPAuth = true; 41 | $this->mail->Username = EMAIL_SENDER_USERNAME; 42 | $this->mail->Password = EMAIL_SENDER_PASSWORD; 43 | $this->mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; 44 | $this->mail->Port = 587; 45 | 46 | $this->mail->setFrom(EMAIL_SENDER, 'Aprelendo'); 47 | } // end __construct() 48 | } 49 | -------------------------------------------------------------------------------- /src/Includes/Classes/InternalException.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | namespace Aprelendo; 22 | 23 | use Exception; 24 | 25 | class InternalException extends Exception { 26 | public function getJsonError(): string 27 | { 28 | return $this->encodeJsonError('Oops! There was an unexpected error processing your request.'); 29 | } 30 | 31 | protected function encodeJsonError(string $error_msg): string 32 | { 33 | $error = ['error_msg' => $error_msg]; 34 | 35 | // Set the JSON content type header 36 | header('Content-Type: application/json'); 37 | 38 | // Encode the error array as JSON and return it 39 | return json_encode($error); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Includes/Classes/Log.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | namespace Aprelendo; 22 | 23 | abstract class Log extends DBEntity 24 | { 25 | private int $user_id = 0; 26 | 27 | /** 28 | * Constructor 29 | * 30 | * @param \PDO $pdo 31 | * @param int $user_id 32 | * 33 | */ 34 | public function __construct(\PDO $pdo, int $user_id) 35 | { 36 | parent::__construct($pdo); 37 | $this->user_id = $user_id; 38 | } // end __construct() 39 | 40 | /** 41 | * Gets today's records for the current user in the log table 42 | * 43 | * @return int 44 | */ 45 | public function countTodayRecords(): int 46 | { 47 | $sql = "SELECT COUNT(*) AS `exists` 48 | FROM `{$this->table}` 49 | WHERE `user_id` = ? 50 | AND `date_created` = CURRENT_DATE()"; 51 | 52 | $row = $this->sqlFetch($sql, [$this->user_id]); 53 | 54 | return $row['exists']; 55 | } // end countTodayRecords() 56 | 57 | /** 58 | * Adds log record for current user 59 | * 60 | * @return bool 61 | */ 62 | public function addRecord(): void 63 | { 64 | $sql = "INSERT INTO `{$this->table}` (`user_id`, `date_created`) VALUES (?, CURRENT_DATE())"; 65 | $this->sqlExecute($sql, [$this->user_id]); 66 | 67 | $this->purgeOldRecords(); // if successful, purge old records 68 | } // end addRecord() 69 | 70 | /** 71 | * Remove old log records 72 | * 73 | * @return void 74 | */ 75 | private function purgeOldRecords() 76 | { 77 | $sql = "DELETE FROM `{$this->table}` WHERE `date_created` < NOW() - INTERVAL 2 DAY"; 78 | $this->sqlExecute($sql, []); 79 | } // end purgeOldRecords() 80 | } 81 | -------------------------------------------------------------------------------- /src/Includes/Classes/LogAudioStreams.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | namespace Aprelendo; 22 | 23 | class LogAudioStreams extends Log 24 | { 25 | /** 26 | * Constructor 27 | * 28 | * @param \PDO $pdo 29 | * @param int $user_id 30 | */ 31 | public function __construct(\PDO $pdo, int $user_id) 32 | { 33 | parent::__construct($pdo, $user_id); 34 | $this->table = 'log_audio_streams'; 35 | } // end __construct() 36 | } 37 | -------------------------------------------------------------------------------- /src/Includes/Classes/LogFileUploads.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | namespace Aprelendo; 22 | 23 | class LogFileUploads extends Log 24 | { 25 | /** 26 | * Constructor 27 | * 28 | * @param \PDO $pdo 29 | * @param int $user_id 30 | */ 31 | public function __construct(\PDO $pdo, int $user_id) 32 | { 33 | parent::__construct($pdo, $user_id); 34 | $this->table = 'log_file_uploads'; 35 | } // end __construct() 36 | } 37 | -------------------------------------------------------------------------------- /src/Includes/Classes/SearchParameters.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Aprelendo; 23 | 24 | abstract class SearchParameters 25 | { 26 | public string $search_text; 27 | public int $offset; 28 | public int $limit; 29 | public int $sort_by; 30 | 31 | /** 32 | * Constructor 33 | * 34 | * @param string $text 35 | */ 36 | public function __construct( 37 | string $search_text, 38 | int $offset, 39 | int $limit, 40 | int $sort_by 41 | ) { 42 | $this->search_text = $search_text; 43 | $this->offset = $offset; 44 | $this->limit = $limit; 45 | $this->sort_by = $sort_by; 46 | } 47 | } 48 | 49 | -------------------------------------------------------------------------------- /src/Includes/Classes/SearchWordsParameters.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Aprelendo; 23 | 24 | class SearchWordsParameters extends SearchParameters 25 | { 26 | /** 27 | * Constructor 28 | * 29 | * @param string $text 30 | */ 31 | public function __construct( 32 | string $search_text, 33 | int $sort_by, 34 | int $offset = 0, 35 | int $limit = 1000000 36 | ) { 37 | parent::__construct($search_text, $offset, $limit, $sort_by); 38 | } 39 | 40 | /** 41 | * Converts sorting patterns selected by user (expressed as an integer value in the sort menu) 42 | * to valid SQL strings 43 | * 44 | * @return string 45 | */ 46 | public function buildSortSQL(): string 47 | { 48 | $result = ''; 49 | switch ($this->sort_by) { 50 | case '0': // new first 51 | $result = '`id` DESC'; 52 | break; 53 | case '1': // old first 54 | $result = '`id`'; 55 | break; 56 | case '2': // learned first 57 | $result = '`status`'; 58 | break; 59 | case '3': // learning first 60 | $result = '`status` DESC'; 61 | break; 62 | case '10': // words first 63 | $result = '`is_phrase` DESC'; 64 | break; 65 | case '11': // phrases first 66 | $result = '`is_phrase`'; 67 | break; 68 | default: 69 | $result = ''; 70 | break; 71 | } 72 | return $result; 73 | } // end buildSortSQL() 74 | } 75 | -------------------------------------------------------------------------------- /src/Includes/Classes/SharedTextTable.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Aprelendo; 23 | 24 | class SharedTextTable extends TextTable 25 | { 26 | /** 27 | * Constructor 28 | * 29 | * @param array $rows 30 | */ 31 | public function __construct(array $rows) 32 | { 33 | parent::__construct($rows, false); 34 | $this->headings = ['Title']; 35 | $this->col_widths = ['69px', '']; 36 | $this->action_menu = []; 37 | $this->sort_menu = [ 38 | 'mSortByNew' => 'New first', 39 | 'mSortByOld' => 'Old first', 40 | 'mSortByMoreLikes' => 'More likes first', 41 | 'mSortByLessLikes' => 'Less likes first' 42 | ]; 43 | $this->is_shared = true; 44 | $this->has_chkbox = false; 45 | } // end __construct() 46 | } 47 | -------------------------------------------------------------------------------- /src/Includes/Classes/UserException.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | namespace Aprelendo; 22 | 23 | class UserException extends InternalException { 24 | public function getJsonError(): string { 25 | return $this->encodeJsonError($this->getMessage()); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/Includes/Classes/UserPassword.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | namespace Aprelendo; 22 | 23 | class UserPassword 24 | { 25 | /** 26 | * Check if $password = $hashed_password 27 | * Meaning the password provided by user is equal to the one stored in the database 28 | * 29 | * @param string $password 30 | * @return boolean 31 | */ 32 | public static function verify(string $password, string $hashed_password): bool 33 | { 34 | try { 35 | if (!password_verify($password, $hashed_password)) { 36 | throw new UserException('Username and password combination is incorrect. Please try again.'); 37 | } 38 | 39 | return true; 40 | } catch (\Exception $e) { 41 | throw new UserException($e->getMessage()); 42 | } 43 | } // end verify() 44 | 45 | /** 46 | * Creates hash for a given password 47 | * 48 | * @param string $password 49 | * @return string 50 | */ 51 | public static function createHash(string $password): string { 52 | return password_hash($password, PASSWORD_BCRYPT, ['cost' => 11]); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Includes/Classes/WordsUtilities.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | namespace Aprelendo; 23 | 24 | abstract class WordsUtilities 25 | { 26 | /** 27 | * Exports words to a CSV file 28 | * 29 | * It exports either the whole set of words corresponding to a user & language combination, 30 | * or the specific subset that results from applying additional filters (e.g. $search_text). 31 | * Results are ordered using $order_by. 32 | * 33 | * @param SearchWordsParameters $search_params 34 | * @return void 35 | */ 36 | public static function exportToCSV(array $words): void 37 | { 38 | $headers = ['Words', 'Status', 'Freq_Level']; 39 | 40 | $fp = fopen('php://output', 'w'); 41 | 42 | if ($fp) { 43 | header('Content-Type: text/csv'); 44 | header('Content-Disposition: attachment; filename="export.csv"'); 45 | header('Pragma: no-cache'); 46 | header('Expires: 0'); 47 | fputcsv($fp, $headers); 48 | 49 | foreach ($words as $word) { 50 | fputcsv($fp, [$word['word'], $word['status'], $word['freq_level']]); 51 | } 52 | 53 | fclose($fp); 54 | } 55 | } // end exportToCSV() 56 | } 57 | -------------------------------------------------------------------------------- /src/Includes/checklogin.php: -------------------------------------------------------------------------------- 1 | . 19 | */ 20 | 21 | use Aprelendo\User; 22 | use Aprelendo\UserAuth; 23 | 24 | if (!isset($user)) { 25 | require_once 'dbinit.php'; // connect to database 26 | 27 | $user = new User($pdo); 28 | $user_auth = new UserAuth($user); 29 | $user_is_logged = $user_auth->isLoggedIn(); 30 | $pdo->exec("SET time_zone='{$user->time_zone}';"); // use user local time zone 31 | 32 | if (!$user_is_logged && !isset($no_redirect)) { 33 | header('Location:/login.php'); 34 | exit; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Includes/dbinit.php: -------------------------------------------------------------------------------- 1 | . 20 | */ 21 | 22 | // perform all database initialization here, in a single file 23 | 24 | require_once dirname(dirname(__FILE__)) . '/config/config.php'; 25 | require_once dirname(APP_ROOT) . '/vendor/autoload.php'; 26 | 27 | use Aprelendo\Connect; 28 | 29 | try { 30 | $db_connection = new Connect(DB_DRIVER, DB_HOST, DB_USER, DB_PASSWORD, DB_NAME, DB_CHARSET); 31 | $pdo = $db_connection->connect(); 32 | } catch (\Exception $e) { 33 | http_response_code(500); 34 | exit; 35 | } 36 | -------------------------------------------------------------------------------- /src/public/401.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 | 9 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |You tried to access a protected resource without providing proper authentication credentials.
31 | 32 |The page or resource you were trying to reach is forbidden for some reason.
31 | 32 |The page you are trying to open could not be found on the server.
31 | 32 |The description of this error pretty much says it all ...
31 | 32 |Error: no data to display