1010use EvolutionCMS \Installer \Utilities \VersionResolver ;
1111use EvolutionCMS \Installer \Validators \PhpValidator ;
1212use GuzzleHttp \Client ;
13- use GuzzleHttp \TransferStats ;
1413use PDOException ;
1514use Symfony \Component \Console \Command \Command ;
1615use Symfony \Component \Console \Input \InputArgument ;
@@ -753,15 +752,35 @@ protected function downloadEvolutionCMS(string $name, array $options, ?string $b
753752 $ isCurrentDir = !empty ($ options ['install_in_current_dir ' ]);
754753 $ targetPath = $ isCurrentDir ? $ name : (getcwd () . '/ ' . $ name );
755754
756- // If branch is specified, download from branch
757- if ($ branch !== null ) {
755+ $ sourceLabel = null ;
756+ $ composerConstraint = null ;
757+ $ composerRepository = null ;
758+ $ preferSource = false ;
759+
760+ if ($ branch !== null && trim ($ branch ) !== '' ) {
758761 $ branch = trim ($ branch );
759- $ this ->tui ->addLog ("Downloading Evolution CMS from branch: {$ branch }... " );
762+ $ this ->tui ->addLog ("Preparing Evolution CMS from branch: {$ branch }... " );
763+
764+ // Packagist for evolution-cms/evolution currently publishes only tagged releases (no dev branches).
765+ // For explicit branches, use the upstream Git repository through Composer.
766+ $ composerRepository = [
767+ // Use explicit "git" to avoid GitHub API auth/rate-limit issues from the GitHub VCS driver.
768+ 'type ' => 'git ' ,
769+ 'url ' => 'https://github.com/evolution-cms/evolution.git ' ,
770+ ];
760771
761- $ downloadUrl = $ versionResolver ->getBranchDownloadUrl ($ branch );
762- $ displayName = $ branch ;
772+ $ preferSource = true ;
773+
774+ // Accept branch aliases like `3.5.x-dev`, otherwise treat as a branch name (`dev-branchName`).
775+ if (str_starts_with ($ branch , 'dev- ' ) || str_ends_with ($ branch , '-dev ' )) {
776+ $ composerConstraint = $ branch ;
777+ } elseif (preg_match ('/^ \\d+ \\. \\d+ \\.x$/ ' , $ branch ) === 1 ) {
778+ $ composerConstraint = $ branch . '-dev ' ;
779+ } else {
780+ $ composerConstraint = 'dev- ' . $ branch ;
781+ }
782+ $ sourceLabel = "branch {$ branch }" ;
763783 } else {
764- // Get latest compatible version
765784 $ this ->tui ->addLog ('Finding compatible Evolution CMS version... ' );
766785 $ phpVersion = PHP_VERSION ;
767786 $ version = $ versionResolver ->getLatestCompatibleVersion ($ phpVersion , true );
@@ -771,184 +790,175 @@ protected function downloadEvolutionCMS(string $name, array $options, ?string $b
771790 throw new \RuntimeException ("Could not find a compatible Evolution CMS version for PHP {$ phpVersion }. " );
772791 }
773792
774- // Remove 'v' prefix if present
775793 $ versionTag = ltrim ($ version , 'v ' );
776794 $ this ->tui ->replaceLastLogs ('<fg=green>✔</> Found compatible version: ' . $ version . '. ' , 2 );
777- $ this ->tui ->addLog ("Downloading Evolution CMS {$ versionTag }... " );
795+ $ this ->tui ->addLog ("Downloading Evolution CMS {$ versionTag } via Composer ... " );
778796
779- $ downloadUrl = $ versionResolver ->getDownloadUrl ($ version );
780- $ displayName = $ versionTag ;
797+ // Use the normalized version without `v` prefix (Packagist versions are `X.Y.Z`).
798+ $ composerConstraint = $ versionTag ;
799+ $ sourceLabel = "version {$ versionTag }" ;
781800 }
782801
783- // Create temp file for download
784- $ tempFile = sys_get_temp_dir () . '/evo-installer- ' . uniqid () . '.zip ' ;
802+ $ tempDir = rtrim (sys_get_temp_dir (), "/ \\" ) . '/evo-installer- ' . uniqid ('' , true );
785803
786804 try {
787- // Download with progress
788- $ this ->downloadFile ($ downloadUrl , $ tempFile );
805+ $ composerWorkDir = rtrim (sys_get_temp_dir (), "/ \\" );
806+ $ composerCommand = $ this ->resolveComposerCommand ($ composerWorkDir );
807+
808+ $ args = [
809+ 'create-project ' ,
810+ $ preferSource ? '--prefer-source ' : '--prefer-dist ' ,
811+ '--no-install ' ,
812+ 'evolution-cms/evolution ' ,
813+ $ tempDir ,
814+ ];
815+ if (is_array ($ composerRepository )) {
816+ $ args [] = '--stability=dev ' ;
817+ $ args [] = '--repository= ' . json_encode ($ composerRepository , JSON_UNESCAPED_SLASHES );
818+ $ args [] = '--remove-vcs ' ;
819+ }
820+ if (is_string ($ composerConstraint ) && trim ($ composerConstraint ) !== '' ) {
821+ $ args [] = trim ($ composerConstraint );
822+ }
789823
790- // Extract archive
791- $ this ->tui ->addLog ('Extracting archive... ' );
792- $ this ->extractZip ($ tempFile , $ targetPath );
824+ $ process = $ this ->runComposer ($ composerCommand , $ args , $ composerWorkDir );
825+ if (!$ process ->isSuccessful ()) {
826+ $ out = $ this ->sanitizeComposerOutput ($ process ->getOutput () . "\n" . $ process ->getErrorOutput ());
827+ throw new \RuntimeException (trim ($ out ) !== '' ? trim ($ out ) : 'Composer create-project failed. ' );
828+ }
829+
830+ $ this ->tui ->replaceLastLogs ('<fg=green>✔</> Download completed. ' );
793831
794- // Clean up
795- @unlink ($ tempFile );
832+ $ this ->tui ->addLog ('Extracting files... ' );
833+ $ this ->copyDirectoryWithProgress ($ tempDir , $ targetPath );
834+ $ this ->removeDirectory ($ tempDir );
796835
797- $ sourceLabel = $ branch ? "branch {$ branch }" : "version {$ displayName }" ;
798836 $ this ->tui ->addLog ("Evolution CMS from {$ sourceLabel } downloaded and extracted successfully! " , 'success ' );
799837
800838 // Mark Step 3 (Download Evolution CMS) as completed
801839 $ this ->steps ['download ' ]['completed ' ] = true ;
802840 $ this ->tui ->setQuestTrack ($ this ->steps );
803841 } catch (\Exception $ e ) {
804842 // Clean up on error
805- @unlink ($ tempFile );
806- $ this ->tui ->addLog ("Failed to download Evolution CMS: " . $ e ->getMessage (), 'error ' );
807- throw $ e ;
808- }
809- }
810-
811- /**
812- * Download file with progress bar.
813- */
814- protected function downloadFile (string $ url , string $ destination ): void
815- {
816- $ client = new Client (['timeout ' => 300 ]);
817-
818- // First, get content length for progress tracking
819- $ totalBytes = 0 ;
820- try {
821- $ headResponse = $ client ->head ($ url , ['allow_redirects ' => true ]);
822- $ totalBytes = (int ) $ headResponse ->getHeaderLine ('Content-Length ' );
823- } catch (\Exception $ e ) {
824- // If HEAD fails, we'll track progress from GET request
825- }
826-
827- $ downloadedBytes = 0 ;
828- $ lastUpdate = 0 ;
829-
830- // Download with on_stats callback for progress tracking
831- $ response = $ client ->get ($ url , [
832- 'sink ' => $ destination ,
833- 'on_stats ' => function (TransferStats $ stats ) use (&$ downloadedBytes , &$ lastUpdate , $ totalBytes ) {
834- $ downloadedBytes = $ stats ->getHandlerStat ('size_download ' ) ?: 0 ;
835- $ totalSize = $ totalBytes > 0 ? $ totalBytes : ($ stats ->getHandlerStat ('size_download ' ) ?: 0 );
836-
837- // Update progress every 100KB or every second
838- $ now = microtime (true );
839- if ($ totalSize > 0 && ($ downloadedBytes - $ lastUpdate > 100000 || $ now - ($ lastUpdate / 1000000 ) > 1 )) {
840- $ this ->tui ->updateProgress ('Downloading ' , $ downloadedBytes , $ totalSize );
841- $ lastUpdate = $ downloadedBytes ;
842- }
843- },
844- ]);
845-
846- // Final progress update
847- if ($ totalBytes > 0 ) {
848- $ this ->tui ->updateProgress ('Downloading ' , $ totalBytes , $ totalBytes );
849- } else {
850- $ actualSize = (int ) $ response ->getHeaderLine ('Content-Length ' );
851- if ($ actualSize > 0 ) {
852- $ this ->tui ->updateProgress ('Downloading ' , $ actualSize , $ actualSize );
843+ if (is_dir ($ tempDir )) {
844+ $ this ->removeDirectory ($ tempDir );
853845 }
854- }
855846
856- $ this ->tui ->replaceLastLogs ('<fg=green>✔</> Download completed. ' );
847+ $ this ->tui ->addLog ('Failed to download Evolution CMS: ' . $ e ->getMessage (), 'error ' );
848+ throw $ e ;
849+ }
857850 }
858851
859852 /**
860- * Extract ZIP archive .
853+ * Copy package files into the destination directory .
861854 */
862- protected function extractZip (string $ zipFile , string $ destination ): void
855+ protected function copyDirectoryWithProgress (string $ source , string $ destination ): void
863856 {
864- if (!extension_loaded ('zip ' )) {
865- throw new \RuntimeException ('ZIP extension is required to extract the archive. ' );
866- }
857+ $ source = rtrim ($ source , "/ \\" );
858+ $ destination = rtrim ($ destination , "/ \\" );
867859
868- $ zip = new \ZipArchive ();
869- if ($ zip ->open ($ zipFile ) !== true ) {
870- throw new \RuntimeException ("Failed to open ZIP archive: {$ zipFile }" );
860+ if (!is_dir ($ source )) {
861+ throw new \RuntimeException ("Temporary download directory not found: {$ source }" );
871862 }
872863
873- // Create destination directory if it doesn't exist
874864 if (!is_dir ($ destination )) {
875- mkdir ($ destination , 0755 , true );
865+ if (!@mkdir ($ destination , 0755 , true ) && !is_dir ($ destination )) {
866+ throw new \RuntimeException ("Failed to create destination directory: {$ destination }" );
867+ }
876868 }
877869
878- // Extract all files (count only entries that will be extracted so progress can reach 100%)
879- $ totalEntries = $ zip ->numFiles ;
880870 $ totalFiles = 0 ;
881- for ($ i = 0 ; $ i < $ totalEntries ; $ i ++) {
882- $ filename = $ zip ->getNameIndex ($ i );
883- if (!$ filename ) {
871+ $ it = new \RecursiveIteratorIterator (
872+ new \RecursiveDirectoryIterator ($ source , \FilesystemIterator::SKIP_DOTS ),
873+ \RecursiveIteratorIterator::SELF_FIRST
874+ );
875+ foreach ($ it as $ item ) {
876+ $ rel = substr ($ item ->getPathname (), strlen ($ source ) + 1 );
877+ if ($ rel === false || $ rel === '' ) {
884878 continue ;
885879 }
886-
887- // Skip directory entries
888- if (substr ($ filename , -1 ) === '/ ' ) {
880+ if ($ this ->shouldSkipCopiedPath ($ rel )) {
889881 continue ;
890882 }
891-
892- // Remove source prefix from path (e.g., "evolution-3.5.0/" or "evolution-branch-name/" -> "")
893- $ localPath = preg_replace ('/^[^\/]+\// ' , '' , $ filename );
894- if (empty ($ localPath )) {
883+ if ($ item ->isDir ()) {
895884 continue ;
896885 }
897-
898886 $ totalFiles ++;
899887 }
900888
901- $ extractedFiles = 0 ;
889+ $ copiedFiles = 0 ;
902890
903- for ($ i = 0 ; $ i < $ totalEntries ; $ i ++) {
904- $ filename = $ zip ->getNameIndex ($ i );
905- if (!$ filename ) {
891+ $ it = new \RecursiveIteratorIterator (
892+ new \RecursiveDirectoryIterator ($ source , \FilesystemIterator::SKIP_DOTS ),
893+ \RecursiveIteratorIterator::SELF_FIRST
894+ );
895+ foreach ($ it as $ item ) {
896+ $ srcPath = $ item ->getPathname ();
897+ $ rel = substr ($ srcPath , strlen ($ source ) + 1 );
898+ if ($ rel === false || $ rel === '' ) {
906899 continue ;
907900 }
908-
909- // Skip directory entries
910- if (substr ($ filename , -1 ) === '/ ' ) {
901+ if ($ this ->shouldSkipCopiedPath ($ rel )) {
911902 continue ;
912903 }
913904
914- // Remove source prefix from path (e.g., "evolution-3.5.0/" or "evolution-branch-name/" -> "")
915- $ localPath = preg_replace ('/^[^\/]+\// ' , '' , $ filename );
905+ $ dstPath = $ destination . '/ ' . str_replace ('\\' , '/ ' , $ rel );
916906
917- if (empty ($ localPath )) {
907+ if ($ item ->isDir ()) {
908+ if (!is_dir ($ dstPath )) {
909+ @mkdir ($ dstPath , 0755 , true );
910+ }
918911 continue ;
919912 }
920913
921- $ targetPath = $ destination . '/ ' . $ localPath ;
922- $ targetDir = dirname ($ targetPath );
923-
924- // Create directory if needed
925- if (!is_dir ($ targetDir )) {
926- mkdir ($ targetDir , 0755 , true );
914+ $ dstDir = dirname ($ dstPath );
915+ if (!is_dir ($ dstDir ) && !@mkdir ($ dstDir , 0755 , true ) && !is_dir ($ dstDir )) {
916+ throw new \RuntimeException ("Failed to create directory: {$ dstDir }" );
927917 }
928918
929- // Extract file
930- $ content = $ zip ->getFromIndex ($ i );
931- if ($ content === false ) {
932- throw new \RuntimeException ("Failed to extract file: {$ filename }" );
933- }
934- if (file_put_contents ($ targetPath , $ content ) === false ) {
935- throw new \RuntimeException ("Failed to write extracted file: {$ targetPath }" );
919+ if ($ item ->isLink ()) {
920+ $ target = @readlink ($ srcPath );
921+ if ($ target === false ) {
922+ throw new \RuntimeException ("Failed to read symlink: {$ srcPath }" );
923+ }
924+ if (is_link ($ dstPath ) || file_exists ($ dstPath )) {
925+ @unlink ($ dstPath );
926+ }
927+ if (!@symlink ($ target , $ dstPath )) {
928+ // Fallback: copy the resolved target content if symlinks are not permitted.
929+ if (!@copy ($ srcPath , $ dstPath )) {
930+ throw new \RuntimeException ("Failed to copy symlink target: {$ srcPath }" );
931+ }
932+ }
933+ } else {
934+ if (!@copy ($ srcPath , $ dstPath )) {
935+ throw new \RuntimeException ("Failed to copy file: {$ rel }" );
936+ }
937+ @chmod ($ dstPath , $ item ->getPerms () & 0777 );
936938 }
937- $ extractedFiles ++;
938939
939- // Update progress every 10 files
940- if ($ totalFiles > 0 && ($ extractedFiles % 10 === 0 || $ extractedFiles === $ totalFiles )) {
941- $ this ->tui ->updateProgress ('Extracting ' , $ extractedFiles , $ totalFiles , 'files ' );
940+ $ copiedFiles ++;
941+ if ($ totalFiles > 0 && ($ copiedFiles % 25 === 0 || $ copiedFiles === $ totalFiles )) {
942+ $ this ->tui ->updateProgress ('Extracting ' , $ copiedFiles , $ totalFiles , 'files ' );
942943 }
943944 }
944945
945- $ zip ->close ();
946-
947- // Ensure progress reaches 100% when extraction finished successfully
948- if ($ totalFiles > 0 && $ extractedFiles === $ totalFiles ) {
946+ if ($ totalFiles > 0 && $ copiedFiles === $ totalFiles ) {
949947 $ this ->tui ->updateProgress ('Extracting ' , $ totalFiles , $ totalFiles , 'files ' );
950948 }
951- $ this ->tui ->replaceLastLogs ('<fg=green>✔</> Extracted ' . $ extractedFiles . ' files. ' );
949+
950+ $ this ->tui ->replaceLastLogs ('<fg=green>✔</> Extracted ' . $ copiedFiles . ' files. ' );
951+ }
952+
953+ protected function shouldSkipCopiedPath (string $ relativePath ): bool
954+ {
955+ $ parts = preg_split ('#[ \\\\/]+# ' , $ relativePath ) ?: [];
956+ foreach ($ parts as $ part ) {
957+ if ($ part === '.git ' || $ part === '.hg ' || $ part === '.svn ' ) {
958+ return true ;
959+ }
960+ }
961+ return false ;
952962 }
953963
954964 /**
0 commit comments